pi-clinepass 0.1.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/package.json +52 -0
- package/src/auth.ts +363 -0
- package/src/catalog.ts +117 -0
- package/src/errors.ts +112 -0
- package/src/headers.ts +80 -0
- package/src/index.ts +109 -0
- package/src/settings.ts +49 -0
- package/src/usage.ts +743 -0
- package/src/workos.ts +231 -0
package/src/workos.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkOS OAuth protocol adapter for ClinePass.
|
|
3
|
+
*
|
|
4
|
+
* Owns: token prefix detection, server-side token refresh, the device-code
|
|
5
|
+
* authorization flow, and credential extraction from Cline CLI / pi auth
|
|
6
|
+
* stores. All I/O is injectable for testability.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
10
|
+
|
|
11
|
+
export const WORKOS_API_BASE = "https://api.workos.com";
|
|
12
|
+
export const WORKOS_CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR";
|
|
13
|
+
export const WORKOS_TOKEN_PREFIX = "workos:";
|
|
14
|
+
export const DEFAULT_API_BASE = "https://api.cline.bot";
|
|
15
|
+
|
|
16
|
+
export const CLINE_REFRESH_ENDPOINT = "/api/v1/auth/refresh";
|
|
17
|
+
export const CLINE_REGISTER_ENDPOINT = "/api/v1/auth/register";
|
|
18
|
+
export const WORKOS_DEVICE_ENDPOINT = "/user_management/authorize/device";
|
|
19
|
+
export const WORKOS_AUTH_ENDPOINT = "/user_management/authenticate";
|
|
20
|
+
|
|
21
|
+
/** Conservative lifetime estimate; WorkOS tokens live ~1 hour. */
|
|
22
|
+
export const WORKOS_TOKEN_LIFETIME_MS = 55 * 60 * 1000;
|
|
23
|
+
/** Refresh 5 minutes before expiry to avoid races. */
|
|
24
|
+
export const WORKOS_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
25
|
+
/** Refresh request timeout. */
|
|
26
|
+
export const WORKOS_REFRESH_TIMEOUT_MS = 15_000;
|
|
27
|
+
|
|
28
|
+
export interface WorkosOptions {
|
|
29
|
+
fetch?: typeof globalThis.fetch;
|
|
30
|
+
apiBase?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isWorkosToken(token: string): boolean {
|
|
34
|
+
return token.startsWith(WORKOS_TOKEN_PREFIX);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Ensure a bare JWT gets the `workos:` prefix the chat API requires. */
|
|
38
|
+
export function ensureWorkosPrefix(token: string): string {
|
|
39
|
+
return isWorkosToken(token) ? token : `${WORKOS_TOKEN_PREFIX}${token}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClineAuthCredentials {
|
|
43
|
+
accessToken: string;
|
|
44
|
+
refreshToken: string;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DeviceAuthorization {
|
|
49
|
+
deviceCode: string;
|
|
50
|
+
userCode: string;
|
|
51
|
+
verificationUri: string;
|
|
52
|
+
verificationUriComplete?: string;
|
|
53
|
+
expiresInSeconds: number;
|
|
54
|
+
intervalSeconds: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Token refresh ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refresh a WorkOS access token via Cline's server-side endpoint.
|
|
61
|
+
* The response is `{ data: { accessToken, refreshToken } }` or flat; the new
|
|
62
|
+
* access token needs the `workos:` prefix when the API returns a bare JWT.
|
|
63
|
+
*/
|
|
64
|
+
export async function refreshWorkosToken(
|
|
65
|
+
credentials: OAuthCredentials,
|
|
66
|
+
options: WorkosOptions = {},
|
|
67
|
+
): Promise<OAuthCredentials> {
|
|
68
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
69
|
+
const apiBase = options.apiBase ?? DEFAULT_API_BASE;
|
|
70
|
+
|
|
71
|
+
let response: Response;
|
|
72
|
+
try {
|
|
73
|
+
response = await fetchFn(`${apiBase}${CLINE_REFRESH_ENDPOINT}`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
granttype: "refresh_token",
|
|
78
|
+
refreshToken: credentials.refresh,
|
|
79
|
+
}),
|
|
80
|
+
signal: AbortSignal.timeout(WORKOS_REFRESH_TIMEOUT_MS),
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
84
|
+
throw new Error("ClinePass token refresh timed out — check your network.", { cause: err });
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const text = await response.text().catch(() => "unknown error");
|
|
91
|
+
throw new Error(`ClinePass token refresh failed (${response.status}): ${text}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = (await response.json()) as {
|
|
95
|
+
data?: { accessToken?: string; refreshToken?: string };
|
|
96
|
+
accessToken?: string;
|
|
97
|
+
refreshToken?: string;
|
|
98
|
+
};
|
|
99
|
+
const tokens = data.data ?? data;
|
|
100
|
+
const accessToken = tokens.accessToken;
|
|
101
|
+
const refreshToken = tokens.refreshToken;
|
|
102
|
+
if (!accessToken || !refreshToken) {
|
|
103
|
+
throw new Error("ClinePass token refresh returned an unexpected response format");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
access: ensureWorkosPrefix(accessToken),
|
|
108
|
+
refresh: refreshToken,
|
|
109
|
+
expires: Date.now() + WORKOS_TOKEN_LIFETIME_MS - WORKOS_REFRESH_MARGIN_MS,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Device-code flow ─────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export async function startDeviceAuthorization(
|
|
116
|
+
options: WorkosOptions = {},
|
|
117
|
+
): Promise<DeviceAuthorization> {
|
|
118
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
119
|
+
const response = await fetchFn(`${WORKOS_API_BASE}${WORKOS_DEVICE_ENDPOINT}`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
122
|
+
body: new URLSearchParams({ client_id: WORKOS_CLIENT_ID }),
|
|
123
|
+
});
|
|
124
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
125
|
+
device_code?: string;
|
|
126
|
+
user_code?: string;
|
|
127
|
+
verification_uri?: string;
|
|
128
|
+
verification_uri_complete?: string;
|
|
129
|
+
expires_in?: number;
|
|
130
|
+
interval?: number;
|
|
131
|
+
error?: string;
|
|
132
|
+
error_description?: string;
|
|
133
|
+
};
|
|
134
|
+
if (!response.ok || !data.device_code || !data.user_code || !data.verification_uri) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Cline device authorization failed: ${data.error_description ?? data.error ?? response.statusText}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
deviceCode: data.device_code,
|
|
141
|
+
userCode: data.user_code,
|
|
142
|
+
verificationUri: data.verification_uri,
|
|
143
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
144
|
+
expiresInSeconds: data.expires_in ?? 300,
|
|
145
|
+
intervalSeconds: data.interval ?? 5,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function pollDeviceAuthorization(
|
|
150
|
+
params: { deviceCode: string; expiresInSeconds: number; intervalSeconds: number },
|
|
151
|
+
options: WorkosOptions = {},
|
|
152
|
+
): Promise<{ accessToken: string; refreshToken: string }> {
|
|
153
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
154
|
+
const deadline = Date.now() + params.expiresInSeconds * 1000;
|
|
155
|
+
let intervalSeconds = Math.max(1, params.intervalSeconds);
|
|
156
|
+
|
|
157
|
+
while (Date.now() <= deadline) {
|
|
158
|
+
const response = await fetchFn(`${WORKOS_API_BASE}${WORKOS_AUTH_ENDPOINT}`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
161
|
+
body: new URLSearchParams({
|
|
162
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
163
|
+
device_code: params.deviceCode,
|
|
164
|
+
client_id: WORKOS_CLIENT_ID,
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
168
|
+
access_token?: string;
|
|
169
|
+
refresh_token?: string;
|
|
170
|
+
error?: string;
|
|
171
|
+
error_description?: string;
|
|
172
|
+
};
|
|
173
|
+
if (response.ok && data.access_token && data.refresh_token) {
|
|
174
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
175
|
+
}
|
|
176
|
+
if (data.error === "authorization_pending") {
|
|
177
|
+
await sleep(intervalSeconds * 1000);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (data.error === "slow_down") {
|
|
181
|
+
// RFC 8628: increase the polling interval by 5 seconds.
|
|
182
|
+
intervalSeconds += 5;
|
|
183
|
+
await sleep(intervalSeconds * 1000);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Cline device authorization failed: ${data.error_description ?? data.error ?? response.statusText}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
throw new Error("Cline device authorization timed out");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Exchange WorkOS tokens for Cline tokens via /api/v1/auth/register. */
|
|
194
|
+
export async function registerWorkOSTokens(
|
|
195
|
+
tokens: { accessToken: string; refreshToken: string },
|
|
196
|
+
options: WorkosOptions = {},
|
|
197
|
+
): Promise<OAuthCredentials> {
|
|
198
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
199
|
+
const apiBase = options.apiBase ?? DEFAULT_API_BASE;
|
|
200
|
+
const response = await fetchFn(`${apiBase}${CLINE_REGISTER_ENDPOINT}`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json", "User-Agent": "pi-clinepass" },
|
|
203
|
+
body: JSON.stringify(tokens),
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const text = await response.text().catch(() => "unknown error");
|
|
207
|
+
throw new Error(`Cline token registration failed (${response.status}): ${text}`);
|
|
208
|
+
}
|
|
209
|
+
const payload = (await response.json()) as {
|
|
210
|
+
success?: boolean;
|
|
211
|
+
data?: { accessToken?: string; refreshToken?: string; expiresAt?: string };
|
|
212
|
+
};
|
|
213
|
+
const data = payload.data;
|
|
214
|
+
if (!payload.success || !data?.accessToken || !data.expiresAt) {
|
|
215
|
+
throw new Error("Invalid token response from Cline");
|
|
216
|
+
}
|
|
217
|
+
const refreshToken = data.refreshToken ?? tokens.refreshToken;
|
|
218
|
+
const expires = Date.parse(data.expiresAt);
|
|
219
|
+
if (Number.isNaN(expires)) {
|
|
220
|
+
throw new Error(`Invalid token expiration from Cline: ${data.expiresAt}`);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
access: ensureWorkosPrefix(data.accessToken),
|
|
224
|
+
refresh: refreshToken,
|
|
225
|
+
expires: expires - WORKOS_REFRESH_MARGIN_MS,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sleep(ms: number): Promise<void> {
|
|
230
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
|
+
}
|