-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathoauth-core.js
More file actions
172 lines (163 loc) · 6.74 KB
/
Copy pathoauth-core.js
File metadata and controls
172 lines (163 loc) · 6.74 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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
if (root) root.CodexOauthCore = Object.freeze(api);
})(typeof window !== "undefined" ? window : globalThis, function () {
const defaultRedirectUri = "http://localhost:1455/auth/callback";
const resumableOauthFlowPhases = new Set(["opening", "waiting"]);
function normalizeOauthCallbackValue(raw, redirectUri = defaultRedirectUri) {
const text = String(raw || "").trim();
if (!text) throw new Error("请先粘贴回调链接。");
const cleaned = text
.replace(/&/g, "&")
.replace(/[\u200B-\u200D\uFEFF]/g, "")
.trim();
const compact = cleaned.replace(/\s+/g, "");
const sources = [cleaned, compact];
for (const source of sources) {
const callbackUrl = source.match(/https?:\/\/(?:localhost|127\.0\.0\.1):1455\/auth\/callback[^\s"'<>]*/i)
|| source.match(/(?:localhost|127\.0\.0\.1):1455\/auth\/callback[^\s"'<>]*/i);
if (callbackUrl) {
const value = callbackUrl[0].replace(/[),.;,。]+$/g, "");
return /^https?:\/\//i.test(value) ? value : `http://${value}`;
}
}
const anyUrl = compact.match(/https?:\/\/[^\s"'<>]+/i);
if (anyUrl) return anyUrl[0].replace(/[),.;,。]+$/g, "");
const paramMatch = compact.match(/(?:^|[?#&])((?:code|error|access_token|accessToken|id_token|idToken|refresh_token|refreshToken)=[^"'<>]+)/);
if (paramMatch) {
const query = paramMatch[1].replace(/^[?#&]/, "");
return `${redirectUri}?${query}`;
}
const bareParam = compact.match(/\b((?:code|error|access_token|accessToken|id_token|idToken|refresh_token|refreshToken)=[^"'<>]+)/);
if (bareParam) return `${redirectUri}?${bareParam[1]}`;
if (/^(?:localhost|127\.0\.0\.1):1455\/auth\/callback/i.test(compact)) return `http://${compact}`;
if (/^https?:\/\//i.test(compact)) return compact;
return `${redirectUri}${compact.startsWith("?") || compact.startsWith("#") ? compact : `?${compact}`}`;
}
function callbackParams(raw, redirectUri = defaultRedirectUri) {
const value = normalizeOauthCallbackValue(raw, redirectUri);
const url = new URL(value);
const params = new URLSearchParams(url.search);
if (url.hash) {
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
const hashParams = new URLSearchParams(hash);
for (const [key, val] of hashParams.entries()) params.set(key, val);
}
return params;
}
function callbackStateStatus(rawOrParams, expectedState = "", redirectUri = defaultRedirectUri) {
const params = rawOrParams instanceof URLSearchParams ? rawOrParams : callbackParams(rawOrParams, redirectUri);
const returnedState = params.get("state") || "";
if (expectedState && !returnedState) {
return {
ok: false,
code: "oauth_state_missing",
state: "",
message: "授权回调缺少本次登录的校验标识。请重新打开授权页面,并只使用刚打开页面返回的回调。",
};
}
if (expectedState && returnedState && returnedState !== expectedState) {
return {
ok: false,
code: "oauth_state_mismatch",
state: returnedState,
message: "收到的授权回调不属于当前这次登录。请重新打开授权页面,并只使用刚打开页面返回的回调。",
};
}
return { ok: true, state: returnedState };
}
function providerErrorStatus(rawOrParams, redirectUri = defaultRedirectUri) {
const params = rawOrParams instanceof URLSearchParams ? rawOrParams : callbackParams(rawOrParams, redirectUri);
const error = params.get("error") || "";
if (!error) return { ok: true };
const rawDescription = params.get("error_description") || params.get("errorDescription") || "";
const description = rawDescription
.replace(/\s+/g, " ")
.trim()
.slice(0, 180);
const friendly = /access_denied/i.test(error)
? "你取消了授权,或授权页面没有完成登录"
: (description || `授权服务返回 ${error}`);
return {
ok: false,
code: "oauth_provider_error",
error,
description,
message: `授权未完成:${friendly}。请重新打开授权页面,并只使用刚打开页面返回的回调。`,
};
}
function exchangeFailureMessage(message) {
const detail = String(message || "换取 token 失败");
if (/could not validate|invalid_grant|expired|code|verifier|already used|已过期|已失效/i.test(detail)) {
return "授权回调已失效、已被使用,或和当前授权链接不匹配";
}
return detail;
}
function emptyCallbackMessage(usedOauthCode = false) {
return usedOauthCode
? "OAuth code 已收到,但没有换到可用授权。请重新打开授权页面。"
: "没有收到有效授权结果。请点击“打开授权页面”重新授权;手动回调只作为自动接收失败后的备用。";
}
function parseOauthFlowSnapshot(raw) {
if (!raw) return null;
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
return raw;
}
function oauthFlowSnapshotStatus(raw, now = Date.now()) {
const snapshot = parseOauthFlowSnapshot(raw);
if (!snapshot?.active) return { ok: false, code: "oauth_flow_missing" };
const phase = String(snapshot.phase || "");
if (!resumableOauthFlowPhases.has(phase)) {
return { ok: false, code: "oauth_flow_not_resumable" };
}
const state = String(snapshot.state || "").trim();
const authUrl = String(snapshot.authUrl || "").trim();
const startedAt = Number(snapshot.startedAt || 0);
const expiresAt = Number(snapshot.expiresAt || 0);
if (!state || !authUrl || !startedAt || !expiresAt) {
return { ok: false, code: "oauth_flow_incomplete" };
}
if (expiresAt <= Number(now || Date.now())) {
return { ok: false, code: "oauth_flow_expired" };
}
let parsedUrl;
try {
parsedUrl = new URL(authUrl);
} catch {
return { ok: false, code: "oauth_authorize_url_invalid" };
}
if (parsedUrl.protocol !== "https:" || parsedUrl.hostname !== "auth.openai.com" || !parsedUrl.pathname.includes("/oauth/authorize")) {
return { ok: false, code: "oauth_authorize_url_invalid" };
}
return {
ok: true,
code: "oauth_flow_resumable",
flow: {
active: true,
phase: "waiting",
state,
authUrl,
startedAt,
expiresAt,
error: "",
summary: "",
},
};
}
return Object.freeze({
normalizeOauthCallbackValue,
callbackParams,
callbackStateStatus,
providerErrorStatus,
exchangeFailureMessage,
emptyCallbackMessage,
oauthFlowSnapshotStatus,
});
});