alchemy 2.0.0-beta.4 → 2.0.0-beta.test-export-fix-4

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.
@@ -1,315 +0,0 @@
1
- import * as Data from "effect/Data";
2
- import * as Effect from "effect/Effect";
3
- import crypto from "node:crypto";
4
- import http from "node:http";
5
- import {
6
- OAUTH_CLIENT_ID,
7
- OAUTH_ENDPOINTS,
8
- OAUTH_REDIRECT_URI,
9
- } from "./AuthProvider.ts";
10
-
11
- export class OAuthError extends Data.TaggedError("OAuthError")<{
12
- error: string;
13
- errorDescription: string;
14
- }> {}
15
-
16
- export interface OAuthCredentials {
17
- type: "oauth";
18
- access: string;
19
- refresh: string;
20
- expires: number;
21
- scopes: string[];
22
- }
23
-
24
- export interface Authorization {
25
- url: string;
26
- state: string;
27
- verifier: string;
28
- }
29
-
30
- function generateState(length = 32): string {
31
- return crypto.randomBytes(length).toString("base64url");
32
- }
33
-
34
- function generatePKCE(length = 96): {
35
- verifier: string;
36
- challenge: string;
37
- } {
38
- const verifier = crypto.randomBytes(length).toString("base64url");
39
- const challenge = crypto
40
- .createHash("sha256")
41
- .update(verifier)
42
- .digest("base64url");
43
- return { verifier, challenge };
44
- }
45
-
46
- function extractCredentials(json: {
47
- access_token: string;
48
- refresh_token: string;
49
- expires_in: number;
50
- scope: string;
51
- }): OAuthCredentials {
52
- return {
53
- type: "oauth",
54
- access: json.access_token,
55
- refresh: json.refresh_token,
56
- expires: Date.now() + json.expires_in * 1000,
57
- scopes: json.scope.split(" "),
58
- };
59
- }
60
-
61
- const tokenRequest = (
62
- body: Record<string, string>,
63
- ): Effect.Effect<OAuthCredentials, OAuthError> =>
64
- Effect.gen(function* () {
65
- const res = yield* Effect.tryPromise({
66
- try: () =>
67
- fetch(OAUTH_ENDPOINTS.token, {
68
- method: "POST",
69
- headers: {
70
- Accept: "application/json",
71
- "Content-Type": "application/x-www-form-urlencoded",
72
- },
73
- body: new URLSearchParams(body).toString(),
74
- }),
75
- catch: (err) =>
76
- new OAuthError({
77
- error: "network_error",
78
- errorDescription: `Token request failed: ${err}`,
79
- }),
80
- });
81
-
82
- if (!res.ok) {
83
- const json = yield* Effect.tryPromise({
84
- try: () =>
85
- res.json() as Promise<{ error: string; error_description: string }>,
86
- catch: () =>
87
- new OAuthError({
88
- error: "parse_error",
89
- errorDescription: `Token endpoint returned ${res.status}`,
90
- }),
91
- });
92
- return yield* new OAuthError({
93
- error: json.error,
94
- errorDescription: json.error_description,
95
- });
96
- }
97
-
98
- const json = yield* Effect.tryPromise({
99
- try: () =>
100
- res.json() as Promise<{
101
- access_token: string;
102
- refresh_token: string;
103
- expires_in: number;
104
- scope: string;
105
- }>,
106
- catch: () =>
107
- new OAuthError({
108
- error: "parse_error",
109
- errorDescription: "Failed to parse token response",
110
- }),
111
- });
112
- return extractCredentials(json);
113
- });
114
-
115
- /**
116
- * Generate an authorization URL with PKCE for the given scopes.
117
- */
118
- export function authorize(scopes: string[]): Authorization {
119
- const state = generateState();
120
- const pkce = generatePKCE();
121
- const url = new URL(OAUTH_ENDPOINTS.authorize);
122
- url.searchParams.set("client_id", OAUTH_CLIENT_ID);
123
- url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
124
- url.searchParams.set("response_type", "code");
125
- url.searchParams.set("scope", scopes.join(" "));
126
- url.searchParams.set("state", state);
127
- url.searchParams.set("code_challenge", pkce.challenge);
128
- url.searchParams.set("code_challenge_method", "S256");
129
- return { url: url.toString(), state, verifier: pkce.verifier };
130
- }
131
-
132
- /**
133
- * Exchange an authorization code for OAuth credentials.
134
- */
135
- export const exchange = (
136
- code: string,
137
- verifier: string,
138
- ): Effect.Effect<OAuthCredentials, OAuthError> =>
139
- tokenRequest({
140
- grant_type: "authorization_code",
141
- code,
142
- code_verifier: verifier,
143
- client_id: OAUTH_CLIENT_ID,
144
- redirect_uri: OAUTH_REDIRECT_URI,
145
- });
146
-
147
- /**
148
- * Refresh expired OAuth credentials.
149
- */
150
- export const refresh = (
151
- credentials: OAuthCredentials,
152
- ): Effect.Effect<OAuthCredentials, OAuthError> =>
153
- tokenRequest({
154
- grant_type: "refresh_token",
155
- refresh_token: credentials.refresh,
156
- client_id: OAUTH_CLIENT_ID,
157
- redirect_uri: OAUTH_REDIRECT_URI,
158
- });
159
-
160
- /**
161
- * Revoke OAuth credentials.
162
- */
163
- export const revoke = (
164
- credentials: OAuthCredentials,
165
- ): Effect.Effect<void, OAuthError> =>
166
- Effect.gen(function* () {
167
- yield* Effect.tryPromise({
168
- try: () =>
169
- fetch(OAUTH_ENDPOINTS.revoke, {
170
- method: "POST",
171
- headers: {
172
- Accept: "application/json",
173
- "Content-Type": "application/x-www-form-urlencoded",
174
- },
175
- body: new URLSearchParams({
176
- refresh_token: credentials.refresh,
177
- client_id: OAUTH_CLIENT_ID,
178
- redirect_uri: OAUTH_REDIRECT_URI,
179
- }).toString(),
180
- }),
181
- catch: (err) =>
182
- new OAuthError({
183
- error: "network_error",
184
- errorDescription: `Revoke request failed: ${err}`,
185
- }),
186
- });
187
- });
188
-
189
- /**
190
- * Start a local HTTP server to listen for the OAuth callback, exchange
191
- * the authorization code, and return the credentials.
192
- *
193
- * Times out after 5 minutes.
194
- */
195
- export const callback = (
196
- authorization: Authorization,
197
- ): Effect.Effect<OAuthCredentials, OAuthError> =>
198
- Effect.tryPromise({
199
- try: () => callbackPromise(authorization),
200
- catch: (err) => {
201
- if (err instanceof OAuthError) return err;
202
- return new OAuthError({
203
- error: "callback_error",
204
- errorDescription: `OAuth callback failed: ${err}`,
205
- });
206
- },
207
- });
208
-
209
- function callbackPromise(
210
- authorization: Authorization,
211
- ): Promise<OAuthCredentials> {
212
- const { pathname, port } = new URL(OAUTH_REDIRECT_URI);
213
-
214
- return new Promise<OAuthCredentials>((resolve, reject) => {
215
- const server = http.createServer(async (req, res) => {
216
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
217
-
218
- if (url.pathname !== pathname) {
219
- res.statusCode = 404;
220
- res.end("Not Found");
221
- return;
222
- }
223
-
224
- const error = url.searchParams.get("error");
225
- const errorDescription = url.searchParams.get("error_description");
226
- if (error) {
227
- res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
228
- res.end();
229
- cleanup();
230
- reject(
231
- new OAuthError({
232
- error,
233
- errorDescription: errorDescription ?? "An unknown error occurred.",
234
- }),
235
- );
236
- return;
237
- }
238
-
239
- const code = url.searchParams.get("code");
240
- const state = url.searchParams.get("state");
241
- if (!code || !state) {
242
- res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
243
- res.end();
244
- cleanup();
245
- reject(
246
- new OAuthError({
247
- error: "invalid_request",
248
- errorDescription: "Missing code or state",
249
- }),
250
- );
251
- return;
252
- }
253
-
254
- if (state !== authorization.state) {
255
- res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
256
- res.end();
257
- cleanup();
258
- reject(
259
- new OAuthError({
260
- error: "invalid_request",
261
- errorDescription: "Invalid state",
262
- }),
263
- );
264
- return;
265
- }
266
-
267
- try {
268
- const credentials = await Effect.runPromise(
269
- exchange(code, authorization.verifier),
270
- );
271
- res.writeHead(302, {
272
- Location: "https://alchemy.run/auth/success",
273
- });
274
- res.end();
275
- cleanup();
276
- resolve(credentials);
277
- } catch (err) {
278
- res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
279
- res.end();
280
- cleanup();
281
- reject(err);
282
- }
283
- });
284
-
285
- const timeout = setTimeout(
286
- () => {
287
- cleanup();
288
- reject(
289
- new OAuthError({
290
- error: "timeout",
291
- errorDescription: "The authorization process timed out.",
292
- }),
293
- );
294
- },
295
- 5 * 60 * 1000,
296
- );
297
-
298
- function cleanup() {
299
- clearTimeout(timeout);
300
- server.close();
301
- }
302
-
303
- server.on("error", (err) => {
304
- cleanup();
305
- reject(
306
- new OAuthError({
307
- error: "server_error",
308
- errorDescription: `Failed to start callback server: ${err.message}`,
309
- }),
310
- );
311
- });
312
-
313
- server.listen(Number(port));
314
- });
315
- }
package/src/Util/Clank.ts DELETED
@@ -1,77 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import * as Data from "effect/Data";
3
- import * as Effect from "effect/Effect";
4
- import { ChildProcess } from "effect/unstable/process";
5
-
6
- export class PromptCancelled extends Data.TaggedError("PromptCancelled") {}
7
- export const retryIfCancelled = Effect.retry({
8
- while: (e: unknown) => e instanceof PromptCancelled,
9
- });
10
-
11
- /**
12
- * Wraps a clack prompt (which returns a `Promise<T | symbol>` where the
13
- * symbol indicates user cancellation) in an Effect.
14
- *
15
- * Returns `undefined` if the user cancels (Ctrl+C / Escape).
16
- *
17
- * Uses `Effect.callback` so fiber interruption propagates via the abort
18
- * signal to any async resources we own; the clack prompt itself is left
19
- * to resolve — its result is ignored after interruption.
20
- */
21
- export const prompt = <T>(
22
- fn: () => Promise<T | symbol>,
23
- ): Effect.Effect<T, PromptCancelled> =>
24
- Effect.callback<T, PromptCancelled>((resume, signal) => {
25
- let settled = false;
26
- fn().then(
27
- (result) => {
28
- if (settled || signal.aborted) return;
29
- settled = true;
30
- if (p.isCancel(result)) {
31
- resume(Effect.fail(new PromptCancelled()));
32
- } else {
33
- resume(Effect.succeed(result as T));
34
- }
35
- },
36
- (err) => {
37
- if (settled || signal.aborted) return;
38
- settled = true;
39
- resume(Effect.die(err));
40
- },
41
- );
42
- });
43
-
44
- export const success = (str: string) => Effect.sync(() => p.log.success(str));
45
- export const warn = (str: string) => Effect.sync(() => p.log.warn(str));
46
- export const error = (str: string) => Effect.sync(() => p.log.error(str));
47
- export const info = (str: string) => Effect.sync(() => p.log.info(str));
48
- export const text = (opts: p.TextOptions) => prompt(() => p.text(opts));
49
- export const password = (opts: p.PasswordOptions) =>
50
- prompt(() => p.password(opts));
51
- export const select = <Value>(opts: p.SelectOptions<Value>) =>
52
- prompt(() => p.select<Value>(opts));
53
- export const confirm = (opts: p.ConfirmOptions) =>
54
- prompt(() => p.confirm(opts));
55
- export const multiselect = <Value>(opts: p.MultiSelectOptions<Value>) =>
56
- prompt(() => p.multiselect<Value>(opts));
57
-
58
- /**
59
- * Open a URL in the user's default browser.
60
- *
61
- * On Windows, uses rundll32's FileProtocolHandler — a built-in shim that
62
- * opens URLs in the default browser. It accepts the URL as a direct
63
- * argument (no shell, no quoting of `&`). cmd.exe `start` would treat
64
- * `&` in OAuth URLs as a command separator, and `explorer.exe` treats
65
- * its arg as a path.
66
- */
67
- export const openUrl = (url: string) =>
68
- Effect.gen(function* () {
69
- const [cmd, args] =
70
- process.platform === "win32"
71
- ? (["rundll32.exe", ["url.dll,FileProtocolHandler", url]] as const)
72
- : process.platform === "darwin"
73
- ? (["open", [url]] as const)
74
- : (["xdg-open", [url]] as const);
75
- const handle = yield* ChildProcess.make(cmd, [...args], { shell: false });
76
- yield* handle.exitCode;
77
- }).pipe(Effect.scoped);