poe-code 4.0.19 → 4.0.21
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/credentials.compile-check.d.ts +1 -0
- package/dist/credentials.compile-check.js +2 -0
- package/dist/credentials.compile-check.js.map +1 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +2586 -0
- package/dist/credentials.js.map +7 -0
- package/dist/index.js +165 -59
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/package.json +5 -1
- package/packages/poe-oauth/dist/index.d.ts +5 -2
- package/packages/poe-oauth/dist/index.js +3 -1
- package/packages/poe-oauth/dist/loopback-authorization.d.ts +7 -0
- package/packages/poe-oauth/dist/loopback-authorization.js +59 -26
- package/packages/poe-oauth/dist/oauth-client.d.ts +17 -0
- package/packages/poe-oauth/dist/oauth-client.js +70 -24
- package/packages/poe-oauth/dist/pkce.d.ts +2 -0
- package/packages/poe-oauth/dist/pkce.js +27 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poe-code",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.21",
|
|
4
4
|
"description": "CLI tool to configure Poe API for developer workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/agent.d.ts",
|
|
20
20
|
"import": "./dist/agent.js"
|
|
21
21
|
},
|
|
22
|
+
"./credentials": {
|
|
23
|
+
"types": "./dist/credentials.d.ts",
|
|
24
|
+
"import": "./dist/credentials.js"
|
|
25
|
+
},
|
|
22
26
|
"./skills": {
|
|
23
27
|
"types": "./dist/skills.d.ts",
|
|
24
28
|
"import": "./dist/skills.js"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { checkAuth } from "./check-auth.js";
|
|
2
|
-
export { createOAuthClient } from "./oauth-client.js";
|
|
2
|
+
export { createOAuthAuthorizationUrl, createOAuthClient, exchangeOAuthCode } from "./oauth-client.js";
|
|
3
|
+
export { validateOAuthAuthorizationCallback } from "./loopback-authorization.js";
|
|
4
|
+
export { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
3
5
|
export type { OAuthLandingPage } from "./loopback-authorization.js";
|
|
4
6
|
export type { AuthIdentity, CheckAuthOptions } from "./check-auth.js";
|
|
5
|
-
export type { OAuthClient, OAuthClientConfig, OAuthResult, OAuthAuthorization } from "./oauth-client.js";
|
|
7
|
+
export type { OAuthClient, OAuthClientConfig, OAuthResult, OAuthAuthorization, CreateOAuthAuthorizationUrlOptions, ExchangeOAuthCodeOptions } from "./oauth-client.js";
|
|
8
|
+
export type { ValidateOAuthAuthorizationCallbackOptions } from "./loopback-authorization.js";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { checkAuth } from "./check-auth.js";
|
|
2
|
-
export { createOAuthClient } from "./oauth-client.js";
|
|
2
|
+
export { createOAuthAuthorizationUrl, createOAuthClient, exchangeOAuthCode } from "./oauth-client.js";
|
|
3
|
+
export { validateOAuthAuthorizationCallback } from "./loopback-authorization.js";
|
|
4
|
+
export { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
@@ -15,6 +15,13 @@ export interface LoopbackAuthorizationSession {
|
|
|
15
15
|
waitForCode(authorizationUrl: string): Promise<string>;
|
|
16
16
|
close(): void;
|
|
17
17
|
}
|
|
18
|
+
export interface ValidateOAuthAuthorizationCallbackOptions {
|
|
19
|
+
callbackUrl: string | URL;
|
|
20
|
+
expectedState: string;
|
|
21
|
+
expectedIssuer?: string;
|
|
22
|
+
requireIssuer?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function validateOAuthAuthorizationCallback(options: ValidateOAuthAuthorizationCallbackOptions): string;
|
|
18
25
|
export declare function createLoopbackAuthorizationSession(options?: LoopbackAuthorizationOptions): Promise<LoopbackAuthorizationSession>;
|
|
19
26
|
export declare function extractCodeFromInput(input: string): string | null;
|
|
20
27
|
export declare function buildSuccessPage(landingPage?: OAuthLandingPage): string;
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { parseAuthorizationState } from "./authorization-state.js";
|
|
3
|
+
export function validateOAuthAuthorizationCallback(options) {
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = typeof options.callbackUrl === "string"
|
|
7
|
+
? new URL(options.callbackUrl)
|
|
8
|
+
: options.callbackUrl;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error("Poe OAuth callbackUrl must be an absolute URL.");
|
|
12
|
+
}
|
|
13
|
+
const callback = {
|
|
14
|
+
code: url.searchParams.get("code"),
|
|
15
|
+
state: url.searchParams.get("state"),
|
|
16
|
+
iss: url.searchParams.get("iss")
|
|
17
|
+
};
|
|
18
|
+
const expected = {
|
|
19
|
+
state: validateExpectedState(options.expectedState),
|
|
20
|
+
issuer: options.expectedIssuer ?? null,
|
|
21
|
+
requireIssuer: options.requireIssuer ?? false
|
|
22
|
+
};
|
|
23
|
+
validateAuthorizationCallbackBinding(callback, expected);
|
|
24
|
+
const authorizationError = url.searchParams.get("error");
|
|
25
|
+
if (authorizationError !== null) {
|
|
26
|
+
const description = url.searchParams.get("error_description");
|
|
27
|
+
const safeError = sanitizeAuthorizationError(authorizationError, expected.state);
|
|
28
|
+
const safeDescription = description === null
|
|
29
|
+
? null
|
|
30
|
+
: sanitizeAuthorizationError(description, expected.state);
|
|
31
|
+
throw new Error(safeDescription === null
|
|
32
|
+
? `OAuth authorization failed: ${safeError}`
|
|
33
|
+
: `OAuth authorization failed: ${safeError} — ${safeDescription}`);
|
|
34
|
+
}
|
|
35
|
+
return validateAuthorizationCallbackParameters(callback, expected);
|
|
36
|
+
}
|
|
3
37
|
export async function createLoopbackAuthorizationSession(options = {}) {
|
|
4
38
|
const callbackPath = options.callbackPath ?? "/callback";
|
|
5
39
|
const server = options.createServer ? options.createServer() : http.createServer();
|
|
@@ -46,39 +80,26 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
|
|
|
46
80
|
res.end("Not found");
|
|
47
81
|
return;
|
|
48
82
|
}
|
|
49
|
-
const error = url.searchParams.get("error");
|
|
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
|
-
}
|
|
62
|
-
const description = url.searchParams.get("error_description") ?? error;
|
|
63
|
-
res.writeHead(400);
|
|
64
|
-
res.end(`Authorization failed: ${description}`);
|
|
65
|
-
settle(() => reject(new Error(`OAuth authorization failed: ${error} — ${description}`)));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
83
|
try {
|
|
69
|
-
const code =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
const code = validateOAuthAuthorizationCallback({
|
|
85
|
+
callbackUrl: url,
|
|
86
|
+
expectedState: expectedAuthorization.state ?? "",
|
|
87
|
+
expectedIssuer: expectedAuthorization.issuer ?? undefined,
|
|
88
|
+
requireIssuer: expectedAuthorization.requireIssuer
|
|
89
|
+
});
|
|
74
90
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
75
91
|
res.end(buildSuccessPage(options.landingPage));
|
|
76
92
|
settle(() => resolve(code));
|
|
77
93
|
}
|
|
78
94
|
catch (error) {
|
|
95
|
+
const validationError = error instanceof Error ? error : new Error(String(error));
|
|
79
96
|
res.writeHead(400);
|
|
80
|
-
res.end(
|
|
81
|
-
|
|
97
|
+
res.end(validationError.message);
|
|
98
|
+
if (url.searchParams.has("error") &&
|
|
99
|
+
!validationError.message.startsWith("OAuth authorization failed:")) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
settle(() => reject(validationError));
|
|
82
103
|
}
|
|
83
104
|
});
|
|
84
105
|
if (options.readLine !== undefined) {
|
|
@@ -171,6 +192,18 @@ function validateAuthorizationCallbackBinding(callback, expected) {
|
|
|
171
192
|
throw new Error("OAuth callback issuer mismatch");
|
|
172
193
|
}
|
|
173
194
|
}
|
|
195
|
+
function validateExpectedState(state) {
|
|
196
|
+
if (state.length === 0 || state.trim() !== state) {
|
|
197
|
+
throw new Error("Poe OAuth expectedState must not be blank or contain surrounding whitespace.");
|
|
198
|
+
}
|
|
199
|
+
return state;
|
|
200
|
+
}
|
|
201
|
+
function sanitizeAuthorizationError(value, expectedState) {
|
|
202
|
+
if (value.includes(expectedState)) {
|
|
203
|
+
return "authorization_error";
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
174
207
|
function escapeHtml(text) {
|
|
175
208
|
return text
|
|
176
209
|
.replaceAll("&", "&")
|
|
@@ -21,4 +21,21 @@ export interface OAuthAuthorization {
|
|
|
21
21
|
export interface OAuthClient {
|
|
22
22
|
authorize(): Promise<OAuthAuthorization>;
|
|
23
23
|
}
|
|
24
|
+
export interface CreateOAuthAuthorizationUrlOptions {
|
|
25
|
+
clientId: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
state: string;
|
|
28
|
+
codeChallenge: string;
|
|
29
|
+
authorizationEndpoint?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ExchangeOAuthCodeOptions {
|
|
32
|
+
clientId: string;
|
|
33
|
+
redirectUri: string;
|
|
34
|
+
code: string;
|
|
35
|
+
codeVerifier: string;
|
|
36
|
+
tokenEndpoint?: string;
|
|
37
|
+
fetch?: typeof globalThis.fetch;
|
|
38
|
+
}
|
|
24
39
|
export declare function createOAuthClient(config: OAuthClientConfig): OAuthClient;
|
|
40
|
+
export declare function createOAuthAuthorizationUrl(options: CreateOAuthAuthorizationUrlOptions): string;
|
|
41
|
+
export declare function exchangeOAuthCode(options: ExchangeOAuthCodeOptions): Promise<OAuthResult>;
|
|
@@ -1,6 +1,6 @@
|
|
|
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, validateCodeChallenge, validateCodeVerifier } 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
6
|
const MAX_VALID_EPOCH_MS = 8_640_000_000_000_000;
|
|
@@ -9,7 +9,9 @@ export function createOAuthClient(config) {
|
|
|
9
9
|
const clientId = validateClientId(config.clientId);
|
|
10
10
|
const normalizedConfig = {
|
|
11
11
|
...config,
|
|
12
|
-
clientId
|
|
12
|
+
clientId,
|
|
13
|
+
authorizationEndpoint: validateHttpUrl(config.authorizationEndpoint ?? DEFAULT_AUTHORIZATION_ENDPOINT, "authorizationEndpoint"),
|
|
14
|
+
tokenEndpoint: validateHttpUrl(config.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT, "tokenEndpoint")
|
|
13
15
|
};
|
|
14
16
|
return {
|
|
15
17
|
authorize: () => startAuthorization(normalizedConfig, fetchFn)
|
|
@@ -33,8 +35,8 @@ async function startAuthorization(config, fetchFn) {
|
|
|
33
35
|
}
|
|
34
36
|
});
|
|
35
37
|
const redirectUri = loopbackSession.redirectUri;
|
|
36
|
-
const authorizationUrl =
|
|
37
|
-
|
|
38
|
+
const authorizationUrl = createOAuthAuthorizationUrl({
|
|
39
|
+
authorizationEndpoint,
|
|
38
40
|
clientId: config.clientId,
|
|
39
41
|
redirectUri,
|
|
40
42
|
codeChallenge,
|
|
@@ -48,13 +50,13 @@ async function startAuthorization(config, fetchFn) {
|
|
|
48
50
|
resultPromise ??= (async () => {
|
|
49
51
|
try {
|
|
50
52
|
const code = await loopbackSession.waitForCode(authorizationUrl);
|
|
51
|
-
return await
|
|
53
|
+
return await exchangeOAuthCode({
|
|
52
54
|
tokenEndpoint,
|
|
53
55
|
code,
|
|
54
56
|
codeVerifier,
|
|
55
57
|
clientId: config.clientId,
|
|
56
58
|
redirectUri,
|
|
57
|
-
fetchFn
|
|
59
|
+
fetch: fetchFn
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
finally {
|
|
@@ -65,41 +67,63 @@ async function startAuthorization(config, fetchFn) {
|
|
|
65
67
|
};
|
|
66
68
|
return { authorizationUrl, waitForResult };
|
|
67
69
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
+
export function createOAuthAuthorizationUrl(options) {
|
|
71
|
+
const clientId = validateClientId(options.clientId);
|
|
72
|
+
const redirectUri = validateHttpUrl(options.redirectUri, "redirectUri");
|
|
73
|
+
const state = validateOpaqueValue(options.state, "state");
|
|
74
|
+
const codeChallenge = validateCodeChallenge(options.codeChallenge);
|
|
75
|
+
const authorizationEndpoint = validateHttpUrl(options.authorizationEndpoint ?? DEFAULT_AUTHORIZATION_ENDPOINT, "authorizationEndpoint");
|
|
76
|
+
const url = new URL(authorizationEndpoint);
|
|
70
77
|
url.searchParams.set("response_type", "code");
|
|
71
|
-
url.searchParams.set("client_id",
|
|
78
|
+
url.searchParams.set("client_id", clientId);
|
|
72
79
|
url.searchParams.set("scope", "apikey:create");
|
|
73
|
-
url.searchParams.set("code_challenge",
|
|
80
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
74
81
|
url.searchParams.set("code_challenge_method", "S256");
|
|
75
|
-
url.searchParams.set("redirect_uri",
|
|
76
|
-
url.searchParams.set("state",
|
|
82
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
83
|
+
url.searchParams.set("state", state);
|
|
77
84
|
return url.toString();
|
|
78
85
|
}
|
|
79
|
-
async function
|
|
86
|
+
export async function exchangeOAuthCode(options) {
|
|
87
|
+
const clientId = validateClientId(options.clientId);
|
|
88
|
+
const redirectUri = validateHttpUrl(options.redirectUri, "redirectUri");
|
|
89
|
+
const code = validateOpaqueValue(options.code, "code");
|
|
90
|
+
const codeVerifier = validateCodeVerifier(options.codeVerifier);
|
|
91
|
+
const tokenEndpoint = validateHttpUrl(options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT, "tokenEndpoint");
|
|
92
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
80
93
|
const body = new URLSearchParams({
|
|
81
94
|
grant_type: "authorization_code",
|
|
82
|
-
code
|
|
83
|
-
code_verifier:
|
|
84
|
-
client_id:
|
|
85
|
-
redirect_uri:
|
|
86
|
-
});
|
|
87
|
-
const response = await params.fetchFn(params.tokenEndpoint, {
|
|
88
|
-
method: "POST",
|
|
89
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
90
|
-
body: body.toString()
|
|
95
|
+
code,
|
|
96
|
+
code_verifier: codeVerifier,
|
|
97
|
+
client_id: clientId,
|
|
98
|
+
redirect_uri: redirectUri
|
|
91
99
|
});
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetchFn(tokenEndpoint, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
|
+
body: body.toString()
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error("Token exchange request failed");
|
|
110
|
+
}
|
|
92
111
|
if (!response.ok) {
|
|
93
112
|
const text = await response.text();
|
|
94
113
|
const description = parseErrorDescription(text);
|
|
95
|
-
|
|
114
|
+
if (description !== null &&
|
|
115
|
+
!description.includes(codeVerifier) &&
|
|
116
|
+
!description.includes(code)) {
|
|
117
|
+
throw new Error(description);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Token exchange failed (${response.status})`);
|
|
96
120
|
}
|
|
97
121
|
let value;
|
|
98
122
|
try {
|
|
99
123
|
value = (await response.json());
|
|
100
124
|
}
|
|
101
125
|
catch (error) {
|
|
102
|
-
throw new Error(`Token exchange failed: invalid JSON response from ${
|
|
126
|
+
throw new Error(`Token exchange failed: invalid JSON response from ${tokenEndpoint}`, {
|
|
103
127
|
cause: error
|
|
104
128
|
});
|
|
105
129
|
}
|
|
@@ -157,6 +181,28 @@ function validateClientId(clientId) {
|
|
|
157
181
|
}
|
|
158
182
|
return clientId;
|
|
159
183
|
}
|
|
184
|
+
function validateHttpUrl(value, field) {
|
|
185
|
+
if (value.trim() !== value || value.length === 0) {
|
|
186
|
+
throw new Error(`Poe OAuth ${field} must be an absolute HTTP(S) URL.`);
|
|
187
|
+
}
|
|
188
|
+
let url;
|
|
189
|
+
try {
|
|
190
|
+
url = new URL(value);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Error(`Poe OAuth ${field} must be an absolute HTTP(S) URL.`);
|
|
194
|
+
}
|
|
195
|
+
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.hash.length > 0) {
|
|
196
|
+
throw new Error(`Poe OAuth ${field} must be an absolute HTTP(S) URL without a fragment.`);
|
|
197
|
+
}
|
|
198
|
+
return url.toString();
|
|
199
|
+
}
|
|
200
|
+
function validateOpaqueValue(value, field) {
|
|
201
|
+
if (value.length === 0 || value.trim() !== value) {
|
|
202
|
+
throw new Error(`Poe OAuth ${field} must not be blank or contain surrounding whitespace.`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
160
206
|
function isValidExpiresIn(value) {
|
|
161
207
|
if (typeof value !== "number" ||
|
|
162
208
|
!Number.isFinite(value) ||
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function generateCodeVerifier(): string;
|
|
2
2
|
export declare function generateCodeChallenge(verifier: string): string;
|
|
3
|
+
export declare function validateCodeVerifier(verifier: string): string;
|
|
4
|
+
export declare function validateCodeChallenge(challenge: string): string;
|
|
@@ -3,5 +3,31 @@ export function generateCodeVerifier() {
|
|
|
3
3
|
return crypto.randomBytes(32).toString("base64url");
|
|
4
4
|
}
|
|
5
5
|
export function generateCodeChallenge(verifier) {
|
|
6
|
-
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
6
|
+
return crypto.createHash("sha256").update(validateCodeVerifier(verifier)).digest("base64url");
|
|
7
|
+
}
|
|
8
|
+
export function validateCodeVerifier(verifier) {
|
|
9
|
+
if (verifier.length < 43 ||
|
|
10
|
+
verifier.length > 128 ||
|
|
11
|
+
!hasOnlyPkceCharacters(verifier)) {
|
|
12
|
+
throw new Error("Poe OAuth codeVerifier must contain 43 to 128 URL-safe PKCE characters.");
|
|
13
|
+
}
|
|
14
|
+
return verifier;
|
|
15
|
+
}
|
|
16
|
+
export function validateCodeChallenge(challenge) {
|
|
17
|
+
if (challenge.length !== 43 || !hasOnlyPkceCharacters(challenge)) {
|
|
18
|
+
throw new Error("Poe OAuth codeChallenge must contain 43 URL-safe PKCE characters.");
|
|
19
|
+
}
|
|
20
|
+
return challenge;
|
|
21
|
+
}
|
|
22
|
+
function hasOnlyPkceCharacters(value) {
|
|
23
|
+
for (const character of value) {
|
|
24
|
+
const code = character.charCodeAt(0);
|
|
25
|
+
const isDigit = code >= 48 && code <= 57;
|
|
26
|
+
const isUppercase = code >= 65 && code <= 90;
|
|
27
|
+
const isLowercase = code >= 97 && code <= 122;
|
|
28
|
+
if (!isDigit && !isUppercase && !isLowercase && character !== "-" && character !== "_") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
7
33
|
}
|