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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.292",
3
+ "version": "3.0.294",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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 Number.MAX_SAFE_INTEGER;
7
+ return MAX_VALID_EPOCH_MS;
7
8
  }
8
- if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn < 0) {
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
- return Date.now() + expiresIn * 1000;
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
- if (typeof expires !== "number" || !Number.isFinite(expires) || expires <= Date.now()) {
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 && email.length > 0;
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
- || balance === null
47
- || (typeof balance === "number" && Number.isFinite(balance));
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
- return typeof value === "string" && value.length > 0 ? value : undefined;
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.readLine().then((input) => {
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
- }).catch((error) => {
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
- && callback.iss.length > 0
151
- && expected.issuer !== null
152
- && callback.iss !== expected.issuer) {
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("&", "&amp;")
160
177
  .replaceAll("<", "&lt;")
161
178
  .replaceAll(">", "&gt;")
162
- .replaceAll("\"", "&quot;");
179
+ .replaceAll('"', "&quot;");
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
- "<body style=\"font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0\">",
171
- "<div style=\"text-align:center\">",
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, } from "./pkce.js";
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(config, fetchFn)
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.trim().length === 0) {
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
+ }