poe-code 3.0.292 → 3.0.294
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +60 -15
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/opencode-poe-auth/dist/poe-auth-plugin.js +32 -5
- package/packages/poe-oauth/dist/check-auth.js +10 -6
- package/packages/poe-oauth/dist/loopback-authorization.js +33 -16
- package/packages/poe-oauth/dist/oauth-client.js +35 -17
package/package.json
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import open from "open";
|
|
2
2
|
import { createOAuthClient } from "poe-oauth";
|
|
3
3
|
const CLIENT_ID = "client_728290227fc048cc9262091a1ea197ea";
|
|
4
|
+
const MAX_VALID_EPOCH_MS = 8_640_000_000_000_000;
|
|
4
5
|
function getExpiry(expiresIn) {
|
|
5
6
|
if (expiresIn === null) {
|
|
6
|
-
return
|
|
7
|
+
return MAX_VALID_EPOCH_MS;
|
|
7
8
|
}
|
|
8
|
-
if (typeof expiresIn !== "number" ||
|
|
9
|
+
if (typeof expiresIn !== "number" ||
|
|
10
|
+
!Number.isFinite(expiresIn) ||
|
|
11
|
+
!Number.isInteger(expiresIn) ||
|
|
12
|
+
expiresIn < 0) {
|
|
9
13
|
throw new Error("Poe API key has invalid expiration metadata. Run `opencode providers login` again.");
|
|
10
14
|
}
|
|
11
|
-
|
|
15
|
+
const expires = Date.now() + expiresIn * 1000;
|
|
16
|
+
assertValidExpiryTimestamp(expires);
|
|
17
|
+
return expires;
|
|
12
18
|
}
|
|
13
19
|
function requireApiKey(value) {
|
|
14
20
|
const apiKey = typeof value === "string" ? value.trim() : "";
|
|
15
21
|
if (apiKey.length === 0) {
|
|
16
22
|
throw new Error("Poe API key is missing. Run `opencode providers login` again.");
|
|
17
23
|
}
|
|
24
|
+
if (hasControlCharacter(apiKey)) {
|
|
25
|
+
throw new Error("Poe API key contains invalid characters. Run `opencode providers login` again.");
|
|
26
|
+
}
|
|
18
27
|
return apiKey;
|
|
19
28
|
}
|
|
20
29
|
async function authorize() {
|
|
@@ -34,7 +43,7 @@ async function authorize() {
|
|
|
34
43
|
instructions: "Complete authorization in your browser. This window will close automatically.",
|
|
35
44
|
method: "auto",
|
|
36
45
|
callback: async () => {
|
|
37
|
-
const result = await authorization.waitForResult();
|
|
46
|
+
const result = (await authorization.waitForResult());
|
|
38
47
|
const resultRecord = isObjectRecord(result) ? result : {};
|
|
39
48
|
const apiKey = requireApiKey(getOwnEntry(resultRecord, "apiKey"));
|
|
40
49
|
return {
|
|
@@ -63,7 +72,8 @@ export async function PoeAuthPlugin(_input) {
|
|
|
63
72
|
return {};
|
|
64
73
|
}
|
|
65
74
|
const expires = getOwnEntry(auth, "expires");
|
|
66
|
-
|
|
75
|
+
assertValidExpiryTimestamp(expires);
|
|
76
|
+
if (expires <= Date.now()) {
|
|
67
77
|
throw new Error("Poe API key expired. Run `opencode providers login` again.");
|
|
68
78
|
}
|
|
69
79
|
return { apiKey: requireApiKey(getOwnEntry(auth, "access")) };
|
|
@@ -95,3 +105,20 @@ function getOwnString(record, key) {
|
|
|
95
105
|
const value = getOwnEntry(record, key);
|
|
96
106
|
return typeof value === "string" ? value : undefined;
|
|
97
107
|
}
|
|
108
|
+
function assertValidExpiryTimestamp(value) {
|
|
109
|
+
if (typeof value !== "number" ||
|
|
110
|
+
!Number.isFinite(value) ||
|
|
111
|
+
!Number.isSafeInteger(value) ||
|
|
112
|
+
value > MAX_VALID_EPOCH_MS) {
|
|
113
|
+
throw new Error("Poe API key has invalid expiration metadata. Run `opencode providers login` again.");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function hasControlCharacter(value) {
|
|
117
|
+
for (const character of value) {
|
|
118
|
+
const code = character.charCodeAt(0);
|
|
119
|
+
if (code <= 31 || code === 127) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
@@ -11,7 +11,7 @@ export async function checkAuth(options) {
|
|
|
11
11
|
if (!response.ok) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
const data = await response.json();
|
|
14
|
+
const data = (await response.json());
|
|
15
15
|
if (!isCurrentBalanceResponse(data)) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
@@ -37,14 +37,14 @@ function isCurrentBalanceResponse(value) {
|
|
|
37
37
|
const data = value;
|
|
38
38
|
const email = getOwnString(data, "email");
|
|
39
39
|
const balance = getOwnEntry(data, "current_point_balance");
|
|
40
|
-
const hasEmail = email !== undefined
|
|
40
|
+
const hasEmail = email !== undefined;
|
|
41
41
|
const hasBalance = balance !== undefined;
|
|
42
42
|
if (!hasEmail && !hasBalance) {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
|
-
return balance === undefined
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
return (balance === undefined ||
|
|
46
|
+
balance === null ||
|
|
47
|
+
(typeof balance === "number" && Number.isFinite(balance) && balance >= 0));
|
|
48
48
|
}
|
|
49
49
|
function getOwnEntry(record, key) {
|
|
50
50
|
return Object.prototype.hasOwnProperty.call(record, key)
|
|
@@ -53,7 +53,11 @@ function getOwnEntry(record, key) {
|
|
|
53
53
|
}
|
|
54
54
|
function getOwnString(record, key) {
|
|
55
55
|
const value = getOwnEntry(record, key);
|
|
56
|
-
|
|
56
|
+
if (typeof value !== "string") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
57
61
|
}
|
|
58
62
|
function getOwnNumber(record, key) {
|
|
59
63
|
const value = getOwnEntry(record, key);
|
|
@@ -13,7 +13,7 @@ export async function createLoopbackAuthorizationSession(options = {}) {
|
|
|
13
13
|
close() {
|
|
14
14
|
server.closeAllConnections?.();
|
|
15
15
|
server.close();
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
async function startServer(server) {
|
|
@@ -48,6 +48,17 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
|
|
|
48
48
|
}
|
|
49
49
|
const error = url.searchParams.get("error");
|
|
50
50
|
if (error !== null) {
|
|
51
|
+
try {
|
|
52
|
+
validateAuthorizationCallbackBinding({
|
|
53
|
+
state: url.searchParams.get("state"),
|
|
54
|
+
iss: url.searchParams.get("iss")
|
|
55
|
+
}, expectedAuthorization);
|
|
56
|
+
}
|
|
57
|
+
catch (validationError) {
|
|
58
|
+
res.writeHead(400);
|
|
59
|
+
res.end(validationError instanceof Error ? validationError.message : "Invalid OAuth callback");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
const description = url.searchParams.get("error_description") ?? error;
|
|
52
63
|
res.writeHead(400);
|
|
53
64
|
res.end(`Authorization failed: ${description}`);
|
|
@@ -58,7 +69,7 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
|
|
|
58
69
|
const code = validateAuthorizationCallbackParameters({
|
|
59
70
|
code: url.searchParams.get("code"),
|
|
60
71
|
state: url.searchParams.get("state"),
|
|
61
|
-
iss: url.searchParams.get("iss")
|
|
72
|
+
iss: url.searchParams.get("iss")
|
|
62
73
|
}, expectedAuthorization);
|
|
63
74
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
64
75
|
res.end(buildSuccessPage(options.landingPage));
|
|
@@ -71,7 +82,9 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
|
|
|
71
82
|
}
|
|
72
83
|
});
|
|
73
84
|
if (options.readLine !== undefined) {
|
|
74
|
-
options
|
|
85
|
+
options
|
|
86
|
+
.readLine()
|
|
87
|
+
.then((input) => {
|
|
75
88
|
const callbackParameters = extractCallbackParametersFromInput(input);
|
|
76
89
|
if (callbackParameters === null) {
|
|
77
90
|
settle(() => reject(new Error("OAuth callback missing authorization code")));
|
|
@@ -84,7 +97,8 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
|
|
|
84
97
|
catch (error) {
|
|
85
98
|
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
86
99
|
}
|
|
87
|
-
})
|
|
100
|
+
})
|
|
101
|
+
.catch((error) => {
|
|
88
102
|
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
89
103
|
});
|
|
90
104
|
}
|
|
@@ -108,14 +122,14 @@ function extractCallbackParametersFromInput(input) {
|
|
|
108
122
|
return {
|
|
109
123
|
code: url.searchParams.get("code"),
|
|
110
124
|
state: url.searchParams.get("state"),
|
|
111
|
-
iss: url.searchParams.get("iss")
|
|
125
|
+
iss: url.searchParams.get("iss")
|
|
112
126
|
};
|
|
113
127
|
}
|
|
114
128
|
catch {
|
|
115
129
|
return {
|
|
116
130
|
code: trimmed,
|
|
117
131
|
state: null,
|
|
118
|
-
iss: null
|
|
132
|
+
iss: null
|
|
119
133
|
};
|
|
120
134
|
}
|
|
121
135
|
}
|
|
@@ -126,13 +140,17 @@ function readExpectedAuthorizationCallback(authorizationUrl) {
|
|
|
126
140
|
return {
|
|
127
141
|
state,
|
|
128
142
|
issuer: parsedState?.issuer ?? null,
|
|
129
|
-
requireIssuer: parsedState?.requireIssuer ?? false
|
|
143
|
+
requireIssuer: parsedState?.requireIssuer ?? false
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
function validateAuthorizationCallbackParameters(callback, expected) {
|
|
133
147
|
if (callback.code === null || callback.code.length === 0) {
|
|
134
148
|
throw new Error("OAuth callback missing authorization code");
|
|
135
149
|
}
|
|
150
|
+
validateAuthorizationCallbackBinding(callback, expected);
|
|
151
|
+
return callback.code;
|
|
152
|
+
}
|
|
153
|
+
function validateAuthorizationCallbackBinding(callback, expected) {
|
|
136
154
|
if (expected.state !== null) {
|
|
137
155
|
if (callback.state === null || callback.state.length === 0) {
|
|
138
156
|
throw new Error("OAuth callback missing state");
|
|
@@ -146,20 +164,19 @@ function validateAuthorizationCallbackParameters(callback, expected) {
|
|
|
146
164
|
throw new Error("OAuth callback missing issuer");
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
|
-
if (callback.iss !== null
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
if (callback.iss !== null &&
|
|
168
|
+
callback.iss.length > 0 &&
|
|
169
|
+
expected.issuer !== null &&
|
|
170
|
+
callback.iss !== expected.issuer) {
|
|
153
171
|
throw new Error("OAuth callback issuer mismatch");
|
|
154
172
|
}
|
|
155
|
-
return callback.code;
|
|
156
173
|
}
|
|
157
174
|
function escapeHtml(text) {
|
|
158
175
|
return text
|
|
159
176
|
.replaceAll("&", "&")
|
|
160
177
|
.replaceAll("<", "<")
|
|
161
178
|
.replaceAll(">", ">")
|
|
162
|
-
.replaceAll("
|
|
179
|
+
.replaceAll('"', """);
|
|
163
180
|
}
|
|
164
181
|
export function buildSuccessPage(landingPage) {
|
|
165
182
|
const title = landingPage?.title ?? "Connected";
|
|
@@ -167,10 +184,10 @@ export function buildSuccessPage(landingPage) {
|
|
|
167
184
|
return [
|
|
168
185
|
"<!DOCTYPE html>",
|
|
169
186
|
`<html><head><meta charset=utf-8><title>${escapeHtml(title)}</title></head>`,
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
'<body style="font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">',
|
|
188
|
+
'<div style="text-align:center">',
|
|
172
189
|
`<h1>${escapeHtml(title)}</h1>`,
|
|
173
190
|
`<p style="color:#666">${escapeHtml(body)}</p>`,
|
|
174
|
-
"</div></body></html>"
|
|
191
|
+
"</div></body></html>"
|
|
175
192
|
].join("");
|
|
176
193
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { createAuthorizationState } from "./authorization-state.js";
|
|
2
2
|
import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
|
|
3
|
-
import { generateCodeChallenge as generatePkceCodeChallenge, generateCodeVerifier as generatePkceCodeVerifier
|
|
3
|
+
import { generateCodeChallenge as generatePkceCodeChallenge, generateCodeVerifier as generatePkceCodeVerifier } from "./pkce.js";
|
|
4
4
|
const DEFAULT_AUTHORIZATION_ENDPOINT = "https://poe.com/oauth/authorize";
|
|
5
5
|
const DEFAULT_TOKEN_ENDPOINT = "https://api.poe.com/token";
|
|
6
|
+
const MAX_VALID_EPOCH_MS = 8_640_000_000_000_000;
|
|
6
7
|
export function createOAuthClient(config) {
|
|
7
8
|
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
9
|
+
const clientId = validateClientId(config.clientId);
|
|
10
|
+
const normalizedConfig = {
|
|
11
|
+
...config,
|
|
12
|
+
clientId
|
|
13
|
+
};
|
|
8
14
|
return {
|
|
9
|
-
authorize: () => startAuthorization(
|
|
15
|
+
authorize: () => startAuthorization(normalizedConfig, fetchFn)
|
|
10
16
|
};
|
|
11
17
|
}
|
|
12
18
|
function generateCodeVerifier() {
|
|
@@ -23,8 +29,8 @@ async function startAuthorization(config, fetchFn) {
|
|
|
23
29
|
createServer: config.createServer,
|
|
24
30
|
landingPage: config.landingPage ?? {
|
|
25
31
|
title: "Connected to Poe",
|
|
26
|
-
body: "You can close this tab and return to your terminal."
|
|
27
|
-
}
|
|
32
|
+
body: "You can close this tab and return to your terminal."
|
|
33
|
+
}
|
|
28
34
|
});
|
|
29
35
|
const redirectUri = loopbackSession.redirectUri;
|
|
30
36
|
const authorizationUrl = buildAuthorizationUrl({
|
|
@@ -34,8 +40,8 @@ async function startAuthorization(config, fetchFn) {
|
|
|
34
40
|
codeChallenge,
|
|
35
41
|
state: createAuthorizationState({
|
|
36
42
|
issuer: new URL(authorizationEndpoint).origin,
|
|
37
|
-
requireIssuer: false
|
|
38
|
-
})
|
|
43
|
+
requireIssuer: false
|
|
44
|
+
})
|
|
39
45
|
});
|
|
40
46
|
let resultPromise;
|
|
41
47
|
const waitForResult = () => {
|
|
@@ -90,33 +96,28 @@ async function exchangeCodeForApiKey(params) {
|
|
|
90
96
|
}
|
|
91
97
|
let value;
|
|
92
98
|
try {
|
|
93
|
-
value = await response.json();
|
|
99
|
+
value = (await response.json());
|
|
94
100
|
}
|
|
95
101
|
catch (error) {
|
|
96
102
|
throw new Error(`Token exchange failed: invalid JSON response from ${params.tokenEndpoint}`, {
|
|
97
|
-
cause: error
|
|
103
|
+
cause: error
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
106
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
101
107
|
throw new Error("Token response must be a JSON object");
|
|
102
108
|
}
|
|
103
109
|
const data = value;
|
|
104
|
-
const apiKey = getOwnString(data, "api_key");
|
|
110
|
+
const apiKey = getOwnString(data, "api_key")?.trim();
|
|
105
111
|
const apiKeyExpiresIn = getOwnEntry(data, "api_key_expires_in");
|
|
106
|
-
if (apiKey === undefined || apiKey.
|
|
112
|
+
if (apiKey === undefined || apiKey.length === 0) {
|
|
107
113
|
throw new Error("Token response missing api_key field");
|
|
108
114
|
}
|
|
109
|
-
if (apiKeyExpiresIn !== undefined
|
|
110
|
-
&& (typeof apiKeyExpiresIn !== "number"
|
|
111
|
-
|| !Number.isFinite(apiKeyExpiresIn)
|
|
112
|
-
|| apiKeyExpiresIn < 0)) {
|
|
115
|
+
if (apiKeyExpiresIn !== undefined && !isValidExpiresIn(apiKeyExpiresIn)) {
|
|
113
116
|
throw new Error("Token response invalid api_key_expires_in field");
|
|
114
117
|
}
|
|
115
118
|
return {
|
|
116
119
|
apiKey,
|
|
117
|
-
expiresIn: typeof apiKeyExpiresIn === "number"
|
|
118
|
-
? apiKeyExpiresIn
|
|
119
|
-
: null
|
|
120
|
+
expiresIn: typeof apiKeyExpiresIn === "number" ? apiKeyExpiresIn : null
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
function parseErrorDescription(text) {
|
|
@@ -149,3 +150,20 @@ function getOwnString(record, key) {
|
|
|
149
150
|
const value = getOwnEntry(record, key);
|
|
150
151
|
return typeof value === "string" ? value : undefined;
|
|
151
152
|
}
|
|
153
|
+
function validateClientId(clientId) {
|
|
154
|
+
const trimmed = clientId.trim();
|
|
155
|
+
if (trimmed.length === 0 || trimmed !== clientId) {
|
|
156
|
+
throw new Error("Poe OAuth clientId must not be blank or contain surrounding whitespace.");
|
|
157
|
+
}
|
|
158
|
+
return clientId;
|
|
159
|
+
}
|
|
160
|
+
function isValidExpiresIn(value) {
|
|
161
|
+
if (typeof value !== "number" ||
|
|
162
|
+
!Number.isFinite(value) ||
|
|
163
|
+
!Number.isInteger(value) ||
|
|
164
|
+
value < 0) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const expiresAt = Date.now() + value * 1000;
|
|
168
|
+
return Number.isSafeInteger(expiresAt) && expiresAt <= MAX_VALID_EPOCH_MS;
|
|
169
|
+
}
|