Repository navigation
Expand file tree
/
Copy pathgithub_error_policy.ts
More file actions
91 lines (80 loc) · 4.22 KB
/
Copy pathgithub_error_policy.ts
File metadata and controls
91 lines (80 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import type { ApplicationErrorCode } from '../../model/application_error';
export interface GithubErrorShape {
status?: number;
message?: string;
response?: unknown;
}
export const getGithubErrorStatus = (error: unknown): number | undefined => {
if (typeof error !== "object" || error === null) return undefined;
const status = (error as GithubErrorShape).status;
return typeof status === "number" ? status : undefined;
};
export const isGithubNotFound = (error: unknown): boolean => getGithubErrorStatus(error) === 404;
/** Maps failed metadata reads without copying untrusted provider text. */
export function githubReadErrorCode(error: unknown): ApplicationErrorCode {
const status = getGithubErrorStatus(error);
if (status === 401) return 'authorization.credential-invalid';
if (isGithubRateLimited(error)) return 'provider.rate-limited';
if (status === 403) return 'authorization.denied';
if (status === 404) return 'provider.not-found';
if (status === 409 || status === 412) return 'provider.conflict';
if (status === 400 || status === 422) return 'provider.contract-invalid';
return 'provider.unavailable';
}
export const isGithubRateLimited = (error: unknown): boolean => {
const status = getGithubErrorStatus(error);
if (status === 429) return true;
if (status !== 403) return false;
const record = readRecord(error);
const response = readRecord(record?.response);
const headers = readRecord(response?.headers);
const message = readRecord(response?.data)?.message ?? record?.message;
return readHeader(headers, 'retry-after') !== undefined
|| readHeader(headers, 'x-ratelimit-remaining') === '0'
|| (typeof message === 'string' && /rate limit|secondary rate|abuse limit|too many requests/i.test(message));
};
export const isGithubPermissionDenied = (error: unknown): boolean => {
if (getGithubErrorStatus(error) !== 403) return false;
const errorRecord = readRecord(error);
const headers = readRecord(readRecord(errorRecord?.response)?.headers);
if (readHeader(headers, 'retry-after') !== undefined) return false;
if (readHeader(headers, 'x-ratelimit-remaining') === '0') return false;
if (readHeader(headers, 'x-github-sso') !== undefined) return false;
const message = errorRecord?.message;
if (typeof message !== 'string') return false;
const normalized = message.trim().toLowerCase();
return normalized === 'forbidden'
|| normalized.includes('resource not accessible by integration')
|| normalized.includes('resource not accessible by personal access token')
|| normalized.includes('permission')
|| normalized.includes('not permitted')
|| normalized.includes('not allowed')
|| normalized.includes('must have admin rights');
};
export const isGithubAlreadyExists = (error: unknown): boolean => {
if (getGithubErrorStatus(error) !== 422) return false;
return hasAlreadyExistsValidationCode(error) || hasAlreadyExistsMessage(error);
};
function hasAlreadyExistsValidationCode(error: unknown): boolean {
const responseData = readRecord(readRecord(error)?.response)?.data;
const validationErrors = readRecord(responseData)?.errors;
return Array.isArray(validationErrors)
&& validationErrors.some((validationError) => readRecord(validationError)?.code === "already_exists");
}
function hasAlreadyExistsMessage(error: unknown): boolean {
const message = readRecord(error)?.message;
if (typeof message !== "string") return false;
const normalized = message.toLowerCase();
return normalized.includes("already exists") || normalized.includes("already_exists");
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined;
}
function readHeader(headers: Record<string, unknown> | undefined, expected: string): string | undefined {
if (!headers) return undefined;
const key = Object.keys(headers).find(candidate => candidate.toLowerCase() === expected);
const value = key ? headers[key] : undefined;
return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined;
}