first commit
This commit is contained in:
565
calcom/packages/app-store/_utils/oauth/OAuthManager.ts
Normal file
565
calcom/packages/app-store/_utils/oauth/OAuthManager.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Manages OAuth2.0 tokens for an app and resourceOwner. It automatically refreshes the token when needed.
|
||||
* It is aware of the credential sync endpoint and can sync the token from the third party source.
|
||||
* It is unaware of Prisma and App logic. It is just a utility to manage OAuth2.0 tokens with life cycle methods
|
||||
*
|
||||
* For a recommended usage example, see Zoom VideoApiAdapter.ts
|
||||
*/
|
||||
import type { z } from "zod";
|
||||
|
||||
import { CREDENTIAL_SYNC_ENDPOINT } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
||||
import type { AxiosLikeResponseToFetchResponse } from "./AxiosLikeResponseToFetchResponse";
|
||||
import type { OAuth2TokenResponseInDbWhenExistsSchema, OAuth2UniversalSchema } from "./universalSchema";
|
||||
import { OAuth2UniversalSchemaWithCalcomBackwardCompatibility } from "./universalSchema";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["app-store/_utils/oauth/OAuthManager"] });
|
||||
export const enum TokenStatus {
|
||||
UNUSABLE_TOKEN_OBJECT,
|
||||
UNUSABLE_ACCESS_TOKEN,
|
||||
INCONCLUSIVE,
|
||||
VALID,
|
||||
}
|
||||
|
||||
type ResourceOwner =
|
||||
| {
|
||||
id: number | null;
|
||||
type: "team";
|
||||
}
|
||||
| {
|
||||
id: number | null;
|
||||
type: "user";
|
||||
};
|
||||
|
||||
type FetchNewTokenObject = ({ refreshToken }: { refreshToken: string | null }) => Promise<Response | null>;
|
||||
type UpdateTokenObject = (
|
||||
token: z.infer<typeof OAuth2UniversalSchemaWithCalcomBackwardCompatibility>
|
||||
) => Promise<void>;
|
||||
type isTokenObjectUnusable = (response: Response) => Promise<{ reason: string } | null>;
|
||||
type isAccessTokenUnusable = (response: Response) => Promise<{ reason: string } | null>;
|
||||
type IsTokenExpired = (token: z.infer<typeof OAuth2UniversalSchema>) => Promise<boolean> | boolean;
|
||||
type InvalidateTokenObject = () => Promise<void>;
|
||||
type ExpireAccessToken = () => Promise<void>;
|
||||
type CredentialSyncVariables = {
|
||||
/**
|
||||
* The secret required to access the credential sync endpoint
|
||||
*/
|
||||
CREDENTIAL_SYNC_SECRET: string | undefined;
|
||||
/**
|
||||
* The header name that the secret should be passed in
|
||||
*/
|
||||
CREDENTIAL_SYNC_SECRET_HEADER_NAME: string;
|
||||
/**
|
||||
* The endpoint where the credential sync should happen
|
||||
*/
|
||||
CREDENTIAL_SYNC_ENDPOINT: string | undefined;
|
||||
|
||||
APP_CREDENTIAL_SHARING_ENABLED: boolean;
|
||||
};
|
||||
/**
|
||||
* Manages OAuth2.0 tokens for an app and resourceOwner
|
||||
* If expiry_date or expires_in isn't provided in token then it is considered expired immediately(if credential sync is not enabled)
|
||||
* If credential sync is enabled, the token is considered expired after a year. It is expected to be refreshed by the API request from the credential source(as it knows when the token is expired)
|
||||
*/
|
||||
export class OAuthManager {
|
||||
private currentTokenObject: z.infer<typeof OAuth2UniversalSchema>;
|
||||
private resourceOwner: ResourceOwner;
|
||||
private appSlug: string;
|
||||
private fetchNewTokenObject: FetchNewTokenObject;
|
||||
private updateTokenObject: UpdateTokenObject;
|
||||
private isTokenObjectUnusable: isTokenObjectUnusable;
|
||||
private isAccessTokenUnusable: isAccessTokenUnusable;
|
||||
private isTokenExpired: IsTokenExpired;
|
||||
private invalidateTokenObject: InvalidateTokenObject;
|
||||
private expireAccessToken: ExpireAccessToken;
|
||||
private credentialSyncVariables: CredentialSyncVariables;
|
||||
private useCredentialSync: boolean;
|
||||
private autoCheckTokenExpiryOnRequest: boolean;
|
||||
|
||||
constructor({
|
||||
resourceOwner,
|
||||
appSlug,
|
||||
currentTokenObject,
|
||||
fetchNewTokenObject,
|
||||
updateTokenObject,
|
||||
isTokenObjectUnusable,
|
||||
isAccessTokenUnusable,
|
||||
invalidateTokenObject,
|
||||
expireAccessToken,
|
||||
credentialSyncVariables,
|
||||
autoCheckTokenExpiryOnRequest = true,
|
||||
isTokenExpired = (token: z.infer<typeof OAuth2TokenResponseInDbWhenExistsSchema>) => {
|
||||
log.debug(
|
||||
"isTokenExpired called",
|
||||
safeStringify({ expiry_date: token.expiry_date, currentTime: Date.now() })
|
||||
);
|
||||
|
||||
return getExpiryDate() <= Date.now();
|
||||
|
||||
function isRelativeToEpoch(relativeTimeInSeconds: number) {
|
||||
return relativeTimeInSeconds > 1000000000; // If it is more than 2001-09-09 it can be considered relative to epoch. Also, that is more than 30 years in future which couldn't possibly be relative to current time
|
||||
}
|
||||
|
||||
function getExpiryDate() {
|
||||
if (token.expiry_date) {
|
||||
return token.expiry_date;
|
||||
}
|
||||
// It is usually in "seconds since now" but due to some integrations logic converting it to "seconds since epoch"(e.g. Office365Calendar has done that) we need to confirm what is the case here.
|
||||
// But we for now know that it is in seconds for sure
|
||||
// If it is not relative to epoch then it would be wrong to use it as it would make the token as non-expired when it could be expired
|
||||
if (token.expires_in && isRelativeToEpoch(token.expires_in)) {
|
||||
return token.expires_in * 1000;
|
||||
}
|
||||
// 0 means it would be expired as Date.now() is greater than that
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
}: {
|
||||
/**
|
||||
* The resource owner for which the token is being managed
|
||||
*/
|
||||
resourceOwner: ResourceOwner;
|
||||
/**
|
||||
* Does response for any request contain information that refresh_token became invalid and thus the entire token object become unusable
|
||||
* Note: Right now, the implementations of this function makes it so that the response is considered invalid(sometimes) even if just access_token is revoked or invalid. In that case, regenerating access token should work. So, we shouldn't mark the token as invalid in that case.
|
||||
* We should instead mark the token as expired. We could do that by introducing isAccessTokenInvalid function
|
||||
*
|
||||
* @param response
|
||||
* @returns
|
||||
*/
|
||||
isTokenObjectUnusable: isTokenObjectUnusable;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
isAccessTokenUnusable: isAccessTokenUnusable;
|
||||
/**
|
||||
* The current token object.
|
||||
*/
|
||||
currentTokenObject: z.infer<typeof OAuth2UniversalSchema>;
|
||||
/**
|
||||
* The unique identifier of the app that the token is for. It is required to do credential syncing in self-hosting
|
||||
*/
|
||||
appSlug: string;
|
||||
/**
|
||||
*
|
||||
* It could be null in case refresh_token isn't available. This is possible when credential sync happens from a third party who doesn't want to share refresh_token and credential syncing has been disabled after the sync has happened.
|
||||
* If credential syncing is still enabled `fetchNewTokenObject` wouldn't be called
|
||||
*/
|
||||
fetchNewTokenObject: FetchNewTokenObject;
|
||||
|
||||
/**
|
||||
* update token object
|
||||
*/
|
||||
updateTokenObject: UpdateTokenObject;
|
||||
/**
|
||||
* Handler to invalidate the token object. It is called when the token object is invalid and credential syncing is disabled
|
||||
*/
|
||||
invalidateTokenObject: InvalidateTokenObject;
|
||||
/*
|
||||
* Handler to expire the access token. It is called when credential syncing is enabled and when the token object expires
|
||||
*/
|
||||
expireAccessToken: ExpireAccessToken;
|
||||
/**
|
||||
* The variables required for credential syncing
|
||||
*/
|
||||
credentialSyncVariables: CredentialSyncVariables;
|
||||
/**
|
||||
* If the token should be checked for expiry before sending a request
|
||||
*/
|
||||
autoCheckTokenExpiryOnRequest?: boolean;
|
||||
/**
|
||||
* If there is a different way to check if the token is expired(and not the standard way of checking expiry_date)
|
||||
*/
|
||||
isTokenExpired?: IsTokenExpired;
|
||||
}) {
|
||||
this.resourceOwner = resourceOwner;
|
||||
this.currentTokenObject = currentTokenObject;
|
||||
this.appSlug = appSlug;
|
||||
this.fetchNewTokenObject = fetchNewTokenObject;
|
||||
this.isTokenObjectUnusable = isTokenObjectUnusable;
|
||||
this.isAccessTokenUnusable = isAccessTokenUnusable;
|
||||
this.isTokenExpired = isTokenExpired;
|
||||
this.invalidateTokenObject = invalidateTokenObject;
|
||||
this.expireAccessToken = expireAccessToken;
|
||||
this.credentialSyncVariables = credentialSyncVariables;
|
||||
this.useCredentialSync = !!(
|
||||
credentialSyncVariables.APP_CREDENTIAL_SHARING_ENABLED &&
|
||||
credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT &&
|
||||
credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME &&
|
||||
credentialSyncVariables.CREDENTIAL_SYNC_SECRET
|
||||
);
|
||||
|
||||
if (this.useCredentialSync) {
|
||||
// Though it should be validated without credential sync as well but it seems like we have some credentials without userId in production
|
||||
// So, we are not validating it for now
|
||||
ensureValidResourceOwner(resourceOwner);
|
||||
}
|
||||
|
||||
this.autoCheckTokenExpiryOnRequest = autoCheckTokenExpiryOnRequest;
|
||||
this.updateTokenObject = updateTokenObject;
|
||||
}
|
||||
|
||||
private isResponseNotOkay(response: Response) {
|
||||
return !response.ok || response.status < 200 || response.status >= 300;
|
||||
}
|
||||
|
||||
public async getTokenObjectOrFetch() {
|
||||
const myLog = log.getSubLogger({
|
||||
prefix: [`getTokenObjectOrFetch:appSlug=${this.appSlug}`],
|
||||
});
|
||||
const isExpired = await this.isTokenExpired(this.currentTokenObject);
|
||||
myLog.debug(
|
||||
"getTokenObjectOrFetch called",
|
||||
safeStringify({
|
||||
isExpired,
|
||||
resourceOwner: this.resourceOwner,
|
||||
})
|
||||
);
|
||||
|
||||
if (!isExpired) {
|
||||
myLog.debug("Token is not expired. Returning the current token object");
|
||||
return { token: this.normalizeNewlyReceivedToken(this.currentTokenObject), isUpdated: false };
|
||||
} else {
|
||||
const token = {
|
||||
// Keep the old token object as it is, as some integrations don't send back all the props e.g. refresh_token isn't sent again by Google Calendar
|
||||
// It also allows any other properties set to be retained.
|
||||
// Let's not use normalizedCurrentTokenObject here as `normalizeToken` could possible be not idempotent
|
||||
...this.currentTokenObject,
|
||||
...this.normalizeNewlyReceivedToken(await this.refreshOAuthToken()),
|
||||
};
|
||||
myLog.debug("Token is expired. So, returning new token object");
|
||||
this.currentTokenObject = token;
|
||||
await this.updateTokenObject(token);
|
||||
return { token, isUpdated: true };
|
||||
}
|
||||
}
|
||||
|
||||
public async request(arg: { url: string; options: RequestInit }): Promise<{
|
||||
tokenStatus: TokenStatus;
|
||||
json: unknown;
|
||||
}>;
|
||||
public async request<T>(
|
||||
customFetch: () => Promise<
|
||||
AxiosLikeResponseToFetchResponse<{
|
||||
status: number;
|
||||
statusText: string;
|
||||
data: T;
|
||||
}>
|
||||
>
|
||||
): Promise<{
|
||||
tokenStatus: TokenStatus;
|
||||
json: T;
|
||||
}>;
|
||||
/**
|
||||
* Send request automatically adding the Authorization header with the access token. More importantly, handles token invalidation
|
||||
*/
|
||||
public async request<T>(
|
||||
customFetchOrUrlAndOptions:
|
||||
| { url: string; options: RequestInit }
|
||||
| (() => Promise<
|
||||
AxiosLikeResponseToFetchResponse<{
|
||||
status: number;
|
||||
statusText: string;
|
||||
data: T;
|
||||
}>
|
||||
>)
|
||||
) {
|
||||
let response;
|
||||
const myLog = log.getSubLogger({ prefix: ["request"] });
|
||||
|
||||
if (this.autoCheckTokenExpiryOnRequest) {
|
||||
await this.getTokenObjectOrFetch();
|
||||
}
|
||||
|
||||
if (typeof customFetchOrUrlAndOptions === "function") {
|
||||
myLog.debug("Sending request using customFetch");
|
||||
const customFetch = customFetchOrUrlAndOptions;
|
||||
try {
|
||||
response = await customFetch();
|
||||
} catch (e) {
|
||||
// Get response from error so that code further down can categorize it into tokenUnusable or access token unusable
|
||||
// Those methods accept response only
|
||||
response = handleFetchError(e);
|
||||
}
|
||||
} else {
|
||||
const { url, options } = customFetchOrUrlAndOptions;
|
||||
const headers = {
|
||||
Authorization: `Bearer ${this.currentTokenObject.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
};
|
||||
myLog.debug("Sending request using fetch", safeStringify({ customFetchOrUrlAndOptions, headers }));
|
||||
// We don't catch fetch error here because such an error would be temporary and we shouldn't take any action on it.
|
||||
response = await fetch(url, {
|
||||
method: "GET",
|
||||
...options,
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
|
||||
myLog.debug(
|
||||
"Response from request",
|
||||
safeStringify({
|
||||
text: await response.clone().text(),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
);
|
||||
|
||||
const { tokenStatus, json } = await this.getAndValidateOAuth2Response({
|
||||
response,
|
||||
});
|
||||
|
||||
if (tokenStatus === TokenStatus.UNUSABLE_TOKEN_OBJECT) {
|
||||
// In case of Credential Sync, we expire the token so that through the sync we can refresh the token
|
||||
// TODO: We should consider sending a special 'reason' query param to toke sync endpoint to convey the reason for getting token
|
||||
await this.invalidate();
|
||||
} else if (tokenStatus === TokenStatus.UNUSABLE_ACCESS_TOKEN) {
|
||||
await this.expireAccessToken();
|
||||
} else if (tokenStatus === TokenStatus.INCONCLUSIVE) {
|
||||
await this.onInconclusiveResponse();
|
||||
}
|
||||
|
||||
// We are done categorizing the token status. Now, we can throw back
|
||||
if ("myFetchError" in (json || {})) {
|
||||
throw new Error(json.myFetchError);
|
||||
}
|
||||
|
||||
return { tokenStatus: tokenStatus, json };
|
||||
}
|
||||
|
||||
/**
|
||||
* It doesn't automatically detect the response for tokenObject and accessToken becoming invalid
|
||||
* Could be used when you expect a possible non JSON response as well.
|
||||
*/
|
||||
public async requestRaw({ url, options }: { url: string; options: RequestInit }) {
|
||||
const myLog = log.getSubLogger({ prefix: ["requestRaw"] });
|
||||
myLog.debug("Sending request using fetch", safeStringify({ url, options }));
|
||||
if (this.autoCheckTokenExpiryOnRequest) {
|
||||
await this.getTokenObjectOrFetch();
|
||||
}
|
||||
const headers = {
|
||||
Authorization: `Bearer ${this.currentTokenObject.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
...options,
|
||||
headers: headers,
|
||||
});
|
||||
myLog.debug(
|
||||
"Response from request",
|
||||
safeStringify({
|
||||
text: await response.clone().text(),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
);
|
||||
if (this.isResponseNotOkay(response)) {
|
||||
await this.onInconclusiveResponse();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private async onInconclusiveResponse() {
|
||||
const myLog = log.getSubLogger({ prefix: ["onInconclusiveResponse"] });
|
||||
myLog.debug("Expiring the access token");
|
||||
// We can't really take any action on inconclusive response
|
||||
// But in case of credential sync we should expire the token so that through the sync we can possibly fix the issue by refreshing the token
|
||||
// It is important because in that cases tokens have an infinite expiry and it is possible that the token is revoked and isAccessUnusable and isTokenObjectUnusable couldn't detect the issue
|
||||
if (this.useCredentialSync) {
|
||||
await this.expireAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
private async invalidate() {
|
||||
const myLog = log.getSubLogger({ prefix: ["invalidate"] });
|
||||
if (this.useCredentialSync) {
|
||||
myLog.debug("Expiring the access token");
|
||||
// We are not calling it through refreshOAuthToken flow because the token is refreshed already there
|
||||
// There is no point expiring the token as we will probably get the same result in that case.
|
||||
await this.expireAccessToken();
|
||||
} else {
|
||||
myLog.debug("Invalidating the token object");
|
||||
// In case credential sync is enabled there is no point of marking the token as invalid as user doesn't take action on that.
|
||||
// The third party needs to sync the correct credential back which we get done by marking the token as expired.
|
||||
await this.invalidateTokenObject();
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeNewlyReceivedToken(
|
||||
token: z.infer<typeof OAuth2UniversalSchemaWithCalcomBackwardCompatibility>
|
||||
) {
|
||||
if (!token.expiry_date && !token.expires_in) {
|
||||
// Use a practically infinite expiry(a year) for when Credential Sync is enabled. Token is expected to be refreshed by the API request from the credential source.
|
||||
// If credential sync is not enabled, we should consider the token as expired otherwise the token could be considered valid forever
|
||||
token.expiry_date = this.useCredentialSync ? Date.now() + 365 * 24 * 3600 * 1000 : 0;
|
||||
} else if (token.expires_in !== undefined && token.expiry_date === undefined) {
|
||||
token.expiry_date = Math.round(Date.now() + token.expires_in * 1000);
|
||||
|
||||
// As expires_in could be relative to current time, we can't keep it in the token object as it could endup giving wrong absolute expiry_time if outdated value is used
|
||||
// That could happen if we merge token objects which we do
|
||||
delete token.expires_in;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// TODO: On regenerating access_token successfully, we should call makeTokenObjectValid(to counter invalidateTokenObject). This should fix stale banner in UI to reconnect when the connection is working
|
||||
private async refreshOAuthToken() {
|
||||
const myLog = log.getSubLogger({ prefix: ["refreshOAuthToken"] });
|
||||
let response;
|
||||
const refreshToken = this.currentTokenObject.refresh_token ?? null;
|
||||
if (this.resourceOwner.id && this.useCredentialSync) {
|
||||
if (
|
||||
!this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET ||
|
||||
!this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME ||
|
||||
!this.credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT
|
||||
) {
|
||||
throw new Error("Credential syncing is enabled but the required env variables are not set");
|
||||
}
|
||||
myLog.debug(
|
||||
"Refreshing OAuth token from credential sync endpoint",
|
||||
safeStringify({
|
||||
appSlug: this.appSlug,
|
||||
resourceOwner: this.resourceOwner,
|
||||
endpoint: CREDENTIAL_SYNC_ENDPOINT,
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
response = await fetch(`${this.credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME]:
|
||||
this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
calcomUserId: this.resourceOwner.id.toString(),
|
||||
appSlug: this.appSlug,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
myLog.error("Could not refresh the token.", safeStringify(e));
|
||||
throw new Error(
|
||||
`Could not refresh the token due to connection issue with the endpoint: ${CREDENTIAL_SYNC_ENDPOINT}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
myLog.debug(
|
||||
"Refreshing OAuth token",
|
||||
safeStringify({
|
||||
appSlug: this.appSlug,
|
||||
resourceOwner: this.resourceOwner,
|
||||
})
|
||||
);
|
||||
try {
|
||||
response = await this.fetchNewTokenObject({ refreshToken });
|
||||
} catch (e) {
|
||||
response = handleFetchError(e);
|
||||
}
|
||||
if (!response) {
|
||||
throw new Error("`fetchNewTokenObject` could not refresh the token");
|
||||
}
|
||||
}
|
||||
|
||||
const clonedResponse = response.clone();
|
||||
myLog.debug(
|
||||
"Response from refreshOAuthToken",
|
||||
safeStringify({
|
||||
text: await clonedResponse.text(),
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
})
|
||||
);
|
||||
|
||||
const { json, tokenStatus } = await this.getAndValidateOAuth2Response({
|
||||
response,
|
||||
});
|
||||
if (tokenStatus === TokenStatus.UNUSABLE_TOKEN_OBJECT) {
|
||||
await this.invalidateTokenObject();
|
||||
} else if (tokenStatus === TokenStatus.UNUSABLE_ACCESS_TOKEN) {
|
||||
await this.expireAccessToken();
|
||||
}
|
||||
const parsedToken = OAuth2UniversalSchemaWithCalcomBackwardCompatibility.safeParse(json);
|
||||
if (!parsedToken.success) {
|
||||
myLog.error("Token parsing error:", safeStringify(parsedToken.error.issues));
|
||||
throw new Error("Invalid token response");
|
||||
}
|
||||
return parsedToken.data;
|
||||
}
|
||||
|
||||
private async getAndValidateOAuth2Response({ response }: { response: Response }) {
|
||||
const myLog = log.getSubLogger({ prefix: ["getAndValidateOAuth2Response"] });
|
||||
const clonedResponse = response.clone();
|
||||
|
||||
// handle empty response (causes crash otherwise on doing json() as "" is invalid JSON) which is valid in some cases like PATCH calls(with 204 response)
|
||||
if ((await clonedResponse.text()).trim() === "") {
|
||||
return { tokenStatus: TokenStatus.VALID, json: null, invalidReason: null } as const;
|
||||
}
|
||||
|
||||
const tokenObjectUsabilityRes = await this.isTokenObjectUnusable(response.clone());
|
||||
const accessTokenUsabilityRes = await this.isAccessTokenUnusable(response.clone());
|
||||
const isNotOkay = this.isResponseNotOkay(response);
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (tokenObjectUsabilityRes?.reason) {
|
||||
myLog.error("Token Object has become unusable");
|
||||
return {
|
||||
tokenStatus: TokenStatus.UNUSABLE_TOKEN_OBJECT,
|
||||
invalidReason: tokenObjectUsabilityRes.reason,
|
||||
json,
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (accessTokenUsabilityRes?.reason) {
|
||||
myLog.error("Access Token has become unusable");
|
||||
return {
|
||||
tokenStatus: TokenStatus.UNUSABLE_ACCESS_TOKEN,
|
||||
invalidReason: accessTokenUsabilityRes?.reason,
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
// Any handlable not ok response should be handled through isTokenObjectUnusable or isAccessTokenUnusable but if still not handled, we should throw an error
|
||||
// So, that the caller can handle it. It could be a network error or some other temporary error from the third party App itself.
|
||||
if (isNotOkay) {
|
||||
return {
|
||||
tokenStatus: TokenStatus.INCONCLUSIVE,
|
||||
invalidReason: response.statusText,
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
return { tokenStatus: TokenStatus.VALID, json, invalidReason: null } as const;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidResourceOwner(
|
||||
resourceOwner: { id: number | null; type: "team" } | { id: number | null; type: "user" }
|
||||
) {
|
||||
if (resourceOwner.type === "team") {
|
||||
throw new Error("Teams are not supported");
|
||||
} else {
|
||||
if (!resourceOwner.id) {
|
||||
throw new Error("resourceOwner should have id set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It converts error into a Response
|
||||
*/
|
||||
function handleFetchError(e: unknown) {
|
||||
const myLog = log.getSubLogger({ prefix: ["handleFetchError"] });
|
||||
myLog.debug("Error", safeStringify(e));
|
||||
if (e instanceof Error) {
|
||||
return new Response(JSON.stringify({ myFetchError: e.message }), { status: 500 });
|
||||
}
|
||||
return new Response(JSON.stringify({ myFetchError: "UNKNOWN_ERROR" }), { status: 500 });
|
||||
}
|
||||
Reference in New Issue
Block a user