alchemy 2.0.0-beta.15 → 2.0.0-beta.16
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/bin/alchemy.js +1585 -707
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/commands/login.ts +6 -3
- package/bin/commands/profile.ts +86 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +2 -3
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.js +12 -8
- package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -1
- package/lib/Cloudflare/ApiToken/Common.d.ts +6 -1
- package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/Common.js.map +1 -1
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +2 -3
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/UserApiToken.js +4 -4
- package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -1
- package/lib/Cloudflare/Artifacts/Artifacts.d.ts +101 -0
- package/lib/Cloudflare/Artifacts/Artifacts.d.ts.map +1 -0
- package/lib/Cloudflare/Artifacts/Artifacts.js +92 -0
- package/lib/Cloudflare/Artifacts/Artifacts.js.map +1 -0
- package/lib/Cloudflare/Artifacts/ArtifactsBinding.d.ts +83 -0
- package/lib/Cloudflare/Artifacts/ArtifactsBinding.d.ts.map +1 -0
- package/lib/Cloudflare/Artifacts/ArtifactsBinding.js +65 -0
- package/lib/Cloudflare/Artifacts/ArtifactsBinding.js.map +1 -0
- package/lib/Cloudflare/Artifacts/index.d.ts +3 -0
- package/lib/Cloudflare/Artifacts/index.d.ts.map +1 -0
- package/lib/Cloudflare/Artifacts/index.js +3 -0
- package/lib/Cloudflare/Artifacts/index.js.map +1 -0
- package/lib/Cloudflare/Container/ContainerBinding.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +3 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/InferEnv.js +0 -1
- package/lib/Cloudflare/Workers/InferEnv.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +2 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +25 -18
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +1 -0
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +1 -0
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/GitHub/AuthProvider.d.ts +37 -0
- package/lib/GitHub/AuthProvider.d.ts.map +1 -0
- package/lib/GitHub/AuthProvider.js +154 -0
- package/lib/GitHub/AuthProvider.js.map +1 -0
- package/lib/GitHub/Credentials.d.ts +29 -0
- package/lib/GitHub/Credentials.d.ts.map +1 -0
- package/lib/GitHub/Credentials.js +54 -0
- package/lib/GitHub/Credentials.js.map +1 -0
- package/lib/GitHub/Providers.d.ts +12 -1
- package/lib/GitHub/Providers.d.ts.map +1 -1
- package/lib/GitHub/Providers.js +13 -3
- package/lib/GitHub/Providers.js.map +1 -1
- package/lib/GitHub/Secret.d.ts +8 -10
- package/lib/GitHub/Secret.d.ts.map +1 -1
- package/lib/GitHub/Secret.js +16 -14
- package/lib/GitHub/Secret.js.map +1 -1
- package/lib/GitHub/Variable.d.ts +8 -10
- package/lib/GitHub/Variable.d.ts.map +1 -1
- package/lib/GitHub/Variable.js +12 -13
- package/lib/GitHub/Variable.js.map +1 -1
- package/lib/GitHub/index.d.ts +2 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +2 -0
- package/lib/GitHub/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Cloudflare/ApiToken/AccountApiToken.ts +21 -11
- package/src/Cloudflare/ApiToken/Common.ts +6 -1
- package/src/Cloudflare/ApiToken/UserApiToken.ts +6 -8
- package/src/Cloudflare/Artifacts/Artifacts.ts +139 -0
- package/src/Cloudflare/Artifacts/ArtifactsBinding.ts +188 -0
- package/src/Cloudflare/Artifacts/index.ts +2 -0
- package/src/Cloudflare/Providers.ts +3 -0
- package/src/Cloudflare/Workers/InferEnv.ts +5 -4
- package/src/Cloudflare/Workers/Worker.ts +12 -1
- package/src/Cloudflare/index.ts +1 -0
- package/src/GitHub/AuthProvider.ts +309 -0
- package/src/GitHub/Credentials.ts +99 -0
- package/src/GitHub/Providers.ts +23 -3
- package/src/GitHub/Secret.ts +22 -24
- package/src/GitHub/Variable.ts +16 -23
- package/src/GitHub/index.ts +2 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Binding from "../../Binding.ts";
|
|
5
|
+
import type { ResourceLike } from "../../Resource.ts";
|
|
6
|
+
import { isWorker, WorkerEnvironment } from "../Workers/Worker.ts";
|
|
7
|
+
import { type Artifacts as ArtifactsLike } from "./Artifacts.ts";
|
|
8
|
+
|
|
9
|
+
export class ArtifactsError extends Data.TaggedError("ArtifactsError")<{
|
|
10
|
+
message: string;
|
|
11
|
+
cause: Error;
|
|
12
|
+
}> {}
|
|
13
|
+
|
|
14
|
+
export type Scope = "read" | "write";
|
|
15
|
+
|
|
16
|
+
export type ArtifactsCreateOptions = {
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
description?: string;
|
|
19
|
+
setDefaultBranch?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ArtifactsImportOptions = {
|
|
23
|
+
source: { url: string; branch?: string; depth?: number };
|
|
24
|
+
target: {
|
|
25
|
+
name: string;
|
|
26
|
+
opts?: { description?: string; readOnly?: boolean };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ArtifactsListOptions = {
|
|
31
|
+
limit?: number;
|
|
32
|
+
cursor?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ArtifactsForkOptions = {
|
|
36
|
+
description?: string;
|
|
37
|
+
readOnly?: boolean;
|
|
38
|
+
defaultBranchOnly?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Effect-native handle to a single Artifacts repo. Wraps the runtime
|
|
43
|
+
* {@link ArtifactsRepo} so each method returns an Effect.
|
|
44
|
+
*/
|
|
45
|
+
export interface ArtifactsRepoClient {
|
|
46
|
+
/** Underlying Cloudflare runtime handle. */
|
|
47
|
+
raw: ArtifactsRepo;
|
|
48
|
+
createToken(
|
|
49
|
+
scope?: Scope,
|
|
50
|
+
ttl?: number,
|
|
51
|
+
): Effect.Effect<ArtifactsCreateTokenResult, ArtifactsError>;
|
|
52
|
+
listTokens(): Effect.Effect<ArtifactsTokenListResult, ArtifactsError>;
|
|
53
|
+
revokeToken(tokenOrId: string): Effect.Effect<boolean, ArtifactsError>;
|
|
54
|
+
fork(
|
|
55
|
+
name: string,
|
|
56
|
+
opts?: ArtifactsForkOptions,
|
|
57
|
+
): Effect.Effect<ArtifactsCreateRepoResult, ArtifactsError>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Effect-native client for a Cloudflare Artifacts namespace binding.
|
|
62
|
+
*
|
|
63
|
+
* Wraps the runtime {@link Artifacts} binding so each method returns an
|
|
64
|
+
* Effect tagged with {@link ArtifactsError}. Use
|
|
65
|
+
* `Cloudflare.ArtifactsBinding.bind(Repos)` inside a Worker's init phase.
|
|
66
|
+
*/
|
|
67
|
+
export interface ArtifactsClient {
|
|
68
|
+
/** Effect resolving to the raw Cloudflare runtime binding. */
|
|
69
|
+
raw: Effect.Effect<Artifacts, never, WorkerEnvironment>;
|
|
70
|
+
create(
|
|
71
|
+
name: string,
|
|
72
|
+
opts?: ArtifactsCreateOptions,
|
|
73
|
+
): Effect.Effect<
|
|
74
|
+
ArtifactsCreateRepoResult,
|
|
75
|
+
ArtifactsError,
|
|
76
|
+
WorkerEnvironment
|
|
77
|
+
>;
|
|
78
|
+
/** Look up an existing repo by name. Fails with `ArtifactsError` if missing. */
|
|
79
|
+
get(
|
|
80
|
+
name: string,
|
|
81
|
+
): Effect.Effect<ArtifactsRepoClient, ArtifactsError, WorkerEnvironment>;
|
|
82
|
+
list(
|
|
83
|
+
opts?: ArtifactsListOptions,
|
|
84
|
+
): Effect.Effect<ArtifactsRepoListResult, ArtifactsError, WorkerEnvironment>;
|
|
85
|
+
delete(
|
|
86
|
+
name: string,
|
|
87
|
+
): Effect.Effect<boolean, ArtifactsError, WorkerEnvironment>;
|
|
88
|
+
import(
|
|
89
|
+
opts: ArtifactsImportOptions,
|
|
90
|
+
): Effect.Effect<
|
|
91
|
+
ArtifactsCreateRepoResult,
|
|
92
|
+
ArtifactsError,
|
|
93
|
+
WorkerEnvironment
|
|
94
|
+
>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class ArtifactsBinding extends Binding.Service<
|
|
98
|
+
ArtifactsBinding,
|
|
99
|
+
(artifacts: ArtifactsLike) => Effect.Effect<ArtifactsClient>
|
|
100
|
+
>()("Cloudflare.Artifacts.Binding") {}
|
|
101
|
+
|
|
102
|
+
export const ArtifactsBindingLive = Layer.effect(
|
|
103
|
+
ArtifactsBinding,
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
const Policy = yield* ArtifactsBindingPolicy;
|
|
106
|
+
|
|
107
|
+
return Effect.fn(function* (artifacts: ArtifactsLike) {
|
|
108
|
+
yield* Policy(artifacts);
|
|
109
|
+
const env = WorkerEnvironment.asEffect();
|
|
110
|
+
const raw = env.pipe(
|
|
111
|
+
Effect.map(
|
|
112
|
+
(env) => (env as Record<string, Artifacts>)[artifacts.name]!,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const use = <T>(
|
|
117
|
+
fn: (raw: Artifacts) => Promise<T>,
|
|
118
|
+
): Effect.Effect<T, ArtifactsError, WorkerEnvironment> =>
|
|
119
|
+
raw.pipe(Effect.flatMap((raw) => tryPromise(() => fn(raw))));
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
raw,
|
|
123
|
+
create: (name, opts) => use((raw) => raw.create(name, opts)),
|
|
124
|
+
get: (name) =>
|
|
125
|
+
use((raw) => raw.get(name)).pipe(
|
|
126
|
+
Effect.flatMap((repo) =>
|
|
127
|
+
repo == null
|
|
128
|
+
? Effect.fail(
|
|
129
|
+
new ArtifactsError({
|
|
130
|
+
message: `Artifacts repo '${name}' not found`,
|
|
131
|
+
cause: new Error("not_found"),
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
: Effect.succeed(wrapRepo(repo as ArtifactsRepo)),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
list: (opts) => use((raw) => raw.list(opts)),
|
|
138
|
+
delete: (name) => use((raw) => raw.delete(name)),
|
|
139
|
+
import: (opts) => use((raw) => raw.import(opts)),
|
|
140
|
+
} satisfies ArtifactsClient;
|
|
141
|
+
});
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export class ArtifactsBindingPolicy extends Binding.Policy<
|
|
146
|
+
ArtifactsBindingPolicy,
|
|
147
|
+
(artifacts: ArtifactsLike) => Effect.Effect<void>
|
|
148
|
+
>()("Cloudflare.Artifacts.Binding") {}
|
|
149
|
+
|
|
150
|
+
export const ArtifactsBindingPolicyLive = ArtifactsBindingPolicy.layer.succeed(
|
|
151
|
+
Effect.fn(function* (host: ResourceLike, artifacts: ArtifactsLike) {
|
|
152
|
+
if (isWorker(host)) {
|
|
153
|
+
yield* host.bind(artifacts.name, {
|
|
154
|
+
bindings: [
|
|
155
|
+
{
|
|
156
|
+
type: "artifacts",
|
|
157
|
+
name: artifacts.name,
|
|
158
|
+
namespace: artifacts.namespace,
|
|
159
|
+
} as any,
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
return yield* Effect.die(
|
|
164
|
+
new Error(`ArtifactsBinding does not support runtime '${host.Type}'`),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const tryPromise = <T>(
|
|
171
|
+
fn: () => Promise<T>,
|
|
172
|
+
): Effect.Effect<T, ArtifactsError> =>
|
|
173
|
+
Effect.tryPromise({
|
|
174
|
+
try: fn,
|
|
175
|
+
catch: (error: any) =>
|
|
176
|
+
new ArtifactsError({
|
|
177
|
+
message: error?.message ?? "Unknown error",
|
|
178
|
+
cause: error,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const wrapRepo = (raw: ArtifactsRepo): ArtifactsRepoClient => ({
|
|
183
|
+
raw,
|
|
184
|
+
createToken: (scope, ttl) => tryPromise(() => raw.createToken(scope, ttl)),
|
|
185
|
+
listTokens: () => tryPromise(() => raw.listTokens()),
|
|
186
|
+
revokeToken: (tokenOrId) => tryPromise(() => raw.revokeToken(tokenOrId)),
|
|
187
|
+
fork: (name, opts) => tryPromise(() => raw.fork(name, opts)),
|
|
188
|
+
});
|
|
@@ -4,6 +4,7 @@ import * as Build from "../Build/index.ts";
|
|
|
4
4
|
import * as Provider from "../Provider.ts";
|
|
5
5
|
import { Random, RandomProvider } from "../Random.ts";
|
|
6
6
|
import * as ApiToken from "./ApiToken/index.ts";
|
|
7
|
+
import * as Artifacts from "./Artifacts/index.ts";
|
|
7
8
|
import { CloudflareAuth } from "./Auth/AuthProvider.ts";
|
|
8
9
|
import * as CloudflareEnvironment from "./CloudflareEnvironment.ts";
|
|
9
10
|
import * as Containers from "./Container/index.ts";
|
|
@@ -33,6 +34,7 @@ export const providers = () =>
|
|
|
33
34
|
Provider.collection([
|
|
34
35
|
ApiToken.AccountApiToken,
|
|
35
36
|
ApiToken.UserApiToken,
|
|
37
|
+
Artifacts.ArtifactsBindingPolicy,
|
|
36
38
|
Command,
|
|
37
39
|
Containers.Container,
|
|
38
40
|
D1.D1ConnectionPolicy,
|
|
@@ -58,6 +60,7 @@ export const providers = () =>
|
|
|
58
60
|
Layer.mergeAll(
|
|
59
61
|
ApiToken.AccountApiTokenProvider(),
|
|
60
62
|
ApiToken.UserApiTokenProvider(),
|
|
63
|
+
Artifacts.ArtifactsBindingPolicyLive,
|
|
61
64
|
Containers.ContainerProvider(),
|
|
62
65
|
D1.D1ConnectionPolicyLive,
|
|
63
66
|
D1.DatabaseProvider(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
// import type * as cf from "@cloudflare/workers-types";
|
|
3
2
|
|
|
4
3
|
import type * as Effect from "effect/Effect";
|
|
5
4
|
import type { UnwrapEffect } from "../../Util/effect.ts";
|
|
@@ -24,6 +23,8 @@ type GetBindingType<T> = T extends Cloudflare.Assets
|
|
|
24
23
|
? KVNamespace
|
|
25
24
|
: T extends Cloudflare.Queue
|
|
26
25
|
? Queue<unknown>
|
|
27
|
-
: T extends Cloudflare.
|
|
28
|
-
?
|
|
29
|
-
:
|
|
26
|
+
: T extends Cloudflare.Artifacts
|
|
27
|
+
? Artifacts
|
|
28
|
+
: T extends Cloudflare.DurableObjectNamespaceLike
|
|
29
|
+
? DurableObjectNamespace<Exclude<T["Shape"], undefined>>
|
|
30
|
+
: never;
|
|
@@ -24,6 +24,10 @@ import Sonda from "sonda/rolldown";
|
|
|
24
24
|
import type * as vite from "vite";
|
|
25
25
|
import * as Artifacts from "../../Artifacts.ts";
|
|
26
26
|
import * as Binding from "../../Binding.ts";
|
|
27
|
+
import {
|
|
28
|
+
isArtifacts as isArtifactsBinding,
|
|
29
|
+
type Artifacts as ArtifactsBinding,
|
|
30
|
+
} from "../Artifacts/Artifacts.ts";
|
|
27
31
|
import { hashDirectory, type MemoOptions } from "../../Build/Memo.ts";
|
|
28
32
|
import * as Bundle from "../../Bundle/Bundle.ts";
|
|
29
33
|
import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
|
|
@@ -183,6 +187,7 @@ export type WorkerBindingResource =
|
|
|
183
187
|
| D1Database
|
|
184
188
|
| KVNamespace
|
|
185
189
|
| CloudflareQueue
|
|
190
|
+
| ArtifactsBinding
|
|
186
191
|
| DurableObjectNamespaceLike<any>;
|
|
187
192
|
|
|
188
193
|
export type WorkerBindings = {
|
|
@@ -667,7 +672,13 @@ export const Worker: Platform<
|
|
|
667
672
|
type: "assets",
|
|
668
673
|
name: bindingName,
|
|
669
674
|
}
|
|
670
|
-
:
|
|
675
|
+
: isArtifactsBinding(binding)
|
|
676
|
+
? ({
|
|
677
|
+
type: "artifacts",
|
|
678
|
+
name: bindingName,
|
|
679
|
+
namespace: binding.namespace,
|
|
680
|
+
} as any)
|
|
681
|
+
: isDurableObjectNamespaceLike(binding)
|
|
671
682
|
? {
|
|
672
683
|
type: "durable_object_namespace",
|
|
673
684
|
name: bindingName,
|
package/src/Cloudflare/index.ts
CHANGED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import * as Console from "effect/Console";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Match from "effect/Match";
|
|
5
|
+
import * as Redacted from "effect/Redacted";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
8
|
+
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
9
|
+
import {
|
|
10
|
+
AuthError,
|
|
11
|
+
AuthProviderLayer,
|
|
12
|
+
type ConfigureContext,
|
|
13
|
+
} from "../Auth/AuthProvider.ts";
|
|
14
|
+
import {
|
|
15
|
+
deleteCredentials,
|
|
16
|
+
displayRedacted,
|
|
17
|
+
readCredentials,
|
|
18
|
+
writeCredentials,
|
|
19
|
+
} from "../Auth/Credentials.ts";
|
|
20
|
+
import { getEnvRedacted, retryOnce } from "../Auth/Env.ts";
|
|
21
|
+
import * as Clank from "../Util/Clank.ts";
|
|
22
|
+
|
|
23
|
+
const options: Array<{
|
|
24
|
+
value: GitHubAuthConfig["method"];
|
|
25
|
+
label: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
}> = [
|
|
28
|
+
{
|
|
29
|
+
value: "gh-cli",
|
|
30
|
+
label: "GitHub CLI",
|
|
31
|
+
hint: "delegate to `gh auth token` (run `gh auth login` first)",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: "env",
|
|
35
|
+
label: "Environment Variables",
|
|
36
|
+
hint: "GITHUB_ACCESS_TOKEN or GITHUB_TOKEN",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "stored",
|
|
40
|
+
label: "Personal Access Token",
|
|
41
|
+
hint: "enter PAT interactively, stored in ~/.alchemy/credentials",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export type GitHubAuthConfig =
|
|
46
|
+
| { method: "env" }
|
|
47
|
+
| { method: "stored" }
|
|
48
|
+
| { method: "gh-cli" };
|
|
49
|
+
|
|
50
|
+
export interface GitHubStoredCredentials {
|
|
51
|
+
type: "pat";
|
|
52
|
+
token: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GitHubResolvedCredentials {
|
|
56
|
+
type: "token";
|
|
57
|
+
token: Redacted.Redacted<string>;
|
|
58
|
+
source: { type: GitHubAuthConfig["method"]; details?: string };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const GITHUB_AUTH_PROVIDER_NAME = "GitHub";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Layer that registers the GitHub {@link AuthProvider} into the
|
|
65
|
+
* {@link AuthProviders} registry when built. Include this in the GitHub
|
|
66
|
+
* `providers()` layer so `alchemy login` can discover it.
|
|
67
|
+
*
|
|
68
|
+
* Supported methods:
|
|
69
|
+
* - `gh-cli`: shells out to `gh auth token` (recommended).
|
|
70
|
+
* - `env`: reads `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN`.
|
|
71
|
+
* - `stored`: prompts for a PAT and writes it to `~/.alchemy/credentials`.
|
|
72
|
+
*
|
|
73
|
+
* Browser/device OAuth is intentionally not implemented: GitHub's
|
|
74
|
+
* OAuth App flow requires a `client_secret` we cannot ship, and
|
|
75
|
+
* device flow is exactly what `gh auth login` already does.
|
|
76
|
+
*/
|
|
77
|
+
export const GitHubAuth = AuthProviderLayer<
|
|
78
|
+
GitHubAuthConfig,
|
|
79
|
+
GitHubResolvedCredentials
|
|
80
|
+
>()(GITHUB_AUTH_PROVIDER_NAME, {
|
|
81
|
+
configure: (profileName, ctx) => configureCredentials(profileName, ctx),
|
|
82
|
+
logout: (profileName, config) => logout(profileName, config),
|
|
83
|
+
login: (profileName, config) => login(profileName, config),
|
|
84
|
+
prettyPrint: (profileName, config) => prettyPrint(profileName, config),
|
|
85
|
+
read: (profileName, config) => resolveCredentials(profileName, config),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const resolveCredentials = (
|
|
89
|
+
profileName: string,
|
|
90
|
+
config: GitHubAuthConfig,
|
|
91
|
+
): Effect.Effect<
|
|
92
|
+
GitHubResolvedCredentials,
|
|
93
|
+
AuthError,
|
|
94
|
+
FileSystem.FileSystem | ChildProcessSpawner
|
|
95
|
+
> =>
|
|
96
|
+
Match.value(config).pipe(
|
|
97
|
+
Match.when(
|
|
98
|
+
{ method: "env" },
|
|
99
|
+
Effect.fnUntraced(function* () {
|
|
100
|
+
const access = yield* getEnvRedacted("GITHUB_ACCESS_TOKEN");
|
|
101
|
+
if (access) {
|
|
102
|
+
return {
|
|
103
|
+
type: "token" as const,
|
|
104
|
+
token: access,
|
|
105
|
+
source: {
|
|
106
|
+
type: "env" as const,
|
|
107
|
+
details: "GITHUB_ACCESS_TOKEN",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const token = yield* getEnvRedacted("GITHUB_TOKEN");
|
|
112
|
+
if (token) {
|
|
113
|
+
return {
|
|
114
|
+
type: "token" as const,
|
|
115
|
+
token,
|
|
116
|
+
source: { type: "env" as const, details: "GITHUB_TOKEN" },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return yield* new AuthError({
|
|
120
|
+
message:
|
|
121
|
+
"GitHub env credentials not found. Set GITHUB_ACCESS_TOKEN or GITHUB_TOKEN.",
|
|
122
|
+
});
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
Match.when({ method: "stored" }, () =>
|
|
126
|
+
readCredentials<GitHubStoredCredentials>(profileName, "gh-stored").pipe(
|
|
127
|
+
Effect.flatMap((creds) =>
|
|
128
|
+
creds == null
|
|
129
|
+
? Effect.fail(
|
|
130
|
+
new AuthError({
|
|
131
|
+
message:
|
|
132
|
+
"GitHub stored credentials not found. Run: alchemy login --configure",
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
: Effect.succeed({
|
|
136
|
+
type: "token" as const,
|
|
137
|
+
token: Redacted.make(creds.token),
|
|
138
|
+
source: { type: "stored" as const },
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
Match.when({ method: "gh-cli" }, () =>
|
|
144
|
+
ghCliToken().pipe(
|
|
145
|
+
Effect.map((token) => ({
|
|
146
|
+
type: "token" as const,
|
|
147
|
+
token: Redacted.make(token),
|
|
148
|
+
source: { type: "gh-cli" as const },
|
|
149
|
+
})),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
Match.exhaustive,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const logout = (profileName: string, config: GitHubAuthConfig) =>
|
|
156
|
+
Match.value(config).pipe(
|
|
157
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
158
|
+
Match.when({ method: "gh-cli" }, () => Effect.void),
|
|
159
|
+
Match.when({ method: "stored" }, () =>
|
|
160
|
+
deleteCredentials(profileName, "gh-stored").pipe(
|
|
161
|
+
Effect.andThen(Clank.success("GitHub: stored credentials removed")),
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
Match.exhaustive,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const login = (profileName: string, config: GitHubAuthConfig) =>
|
|
168
|
+
Match.value(config)
|
|
169
|
+
.pipe(
|
|
170
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
171
|
+
Match.when({ method: "gh-cli" }, () =>
|
|
172
|
+
ghCliToken().pipe(
|
|
173
|
+
Effect.tap(() =>
|
|
174
|
+
Clank.success("GitHub: gh CLI authentication available."),
|
|
175
|
+
),
|
|
176
|
+
Effect.asVoid,
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
Match.when({ method: "stored" }, () =>
|
|
180
|
+
readCredentials<GitHubStoredCredentials>(profileName, "gh-stored").pipe(
|
|
181
|
+
Effect.flatMap((creds) =>
|
|
182
|
+
creds == null ? loginStored(profileName) : Effect.void,
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
Match.exhaustive,
|
|
187
|
+
)
|
|
188
|
+
.pipe(
|
|
189
|
+
Effect.mapError(
|
|
190
|
+
(e) => new AuthError({ message: "login failed", cause: e }),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const configureCredentials = (profileName: string, ctx: ConfigureContext) =>
|
|
195
|
+
Effect.gen(function* () {
|
|
196
|
+
if (ctx.ci) {
|
|
197
|
+
return { method: "env" as const };
|
|
198
|
+
}
|
|
199
|
+
return yield* configureInteractive(profileName);
|
|
200
|
+
}).pipe(
|
|
201
|
+
Effect.mapError(
|
|
202
|
+
(e) =>
|
|
203
|
+
new AuthError({
|
|
204
|
+
message: "failed to configure credentials",
|
|
205
|
+
cause: e,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const configureInteractive = (profileName: string) =>
|
|
211
|
+
Clank.select({
|
|
212
|
+
message: "GitHub authentication method",
|
|
213
|
+
options,
|
|
214
|
+
}).pipe(
|
|
215
|
+
Effect.flatMap((method) =>
|
|
216
|
+
Match.value(method).pipe(
|
|
217
|
+
Match.when("env", () => Effect.succeed({ method: "env" as const })),
|
|
218
|
+
Match.when("gh-cli", () =>
|
|
219
|
+
ghCliToken().pipe(
|
|
220
|
+
Effect.as({ method: "gh-cli" as const }),
|
|
221
|
+
Effect.mapError(
|
|
222
|
+
(e) =>
|
|
223
|
+
new AuthError({
|
|
224
|
+
message: `gh CLI not available: ${e.message}`,
|
|
225
|
+
cause: e,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
Match.when("stored", () => loginStored(profileName)),
|
|
231
|
+
Match.exhaustive,
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const loginStored = Effect.fnUntraced(function* (profileName: string) {
|
|
237
|
+
const token = yield* Clank.password({
|
|
238
|
+
message:
|
|
239
|
+
"GitHub Personal Access Token (needs `repo` scope; `workflow` for Actions)",
|
|
240
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
241
|
+
}).pipe(retryOnce);
|
|
242
|
+
|
|
243
|
+
yield* writeCredentials<GitHubStoredCredentials>(profileName, "gh-stored", {
|
|
244
|
+
type: "pat",
|
|
245
|
+
token,
|
|
246
|
+
});
|
|
247
|
+
yield* Clank.success("GitHub: credentials saved.");
|
|
248
|
+
return { method: "stored" as const };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
class GhCliError extends Error {
|
|
252
|
+
readonly _tag = "GhCliError";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const ghCliToken = () =>
|
|
256
|
+
Effect.gen(function* () {
|
|
257
|
+
const handle = yield* ChildProcess.make("gh", ["auth", "token"], {
|
|
258
|
+
shell: false,
|
|
259
|
+
});
|
|
260
|
+
const [exitCode, stdout, stderr] = yield* Effect.all(
|
|
261
|
+
[
|
|
262
|
+
handle.exitCode,
|
|
263
|
+
Stream.mkString(Stream.decodeText(handle.stdout)),
|
|
264
|
+
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
265
|
+
],
|
|
266
|
+
{ concurrency: 3 },
|
|
267
|
+
);
|
|
268
|
+
if (exitCode !== 0) {
|
|
269
|
+
return yield* Effect.fail(
|
|
270
|
+
new GhCliError(
|
|
271
|
+
`gh auth token exited with ${exitCode}: ${stderr.trim() || stdout.trim()}`,
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const token = stdout.trim();
|
|
276
|
+
if (!token) {
|
|
277
|
+
return yield* Effect.fail(
|
|
278
|
+
new GhCliError("gh auth token returned empty output"),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return token;
|
|
282
|
+
}).pipe(
|
|
283
|
+
Effect.scoped,
|
|
284
|
+
Effect.mapError((e) =>
|
|
285
|
+
e instanceof GhCliError
|
|
286
|
+
? new AuthError({ message: e.message, cause: e })
|
|
287
|
+
: new AuthError({
|
|
288
|
+
message:
|
|
289
|
+
"Could not invoke `gh`. Install GitHub CLI from https://cli.github.com/ and run `gh auth login`.",
|
|
290
|
+
cause: e,
|
|
291
|
+
}),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const prettyPrint = (profileName: string, config: GitHubAuthConfig) =>
|
|
296
|
+
resolveCredentials(profileName, config).pipe(
|
|
297
|
+
Effect.tap((creds) => {
|
|
298
|
+
const sourceStr = creds.source.details
|
|
299
|
+
? `${creds.source.type} - ${creds.source.details}`
|
|
300
|
+
: creds.source.type;
|
|
301
|
+
return Effect.all([
|
|
302
|
+
Console.log(` token: ${displayRedacted(creds.token, 6)}`),
|
|
303
|
+
Console.log(` source: ${sourceStr}`),
|
|
304
|
+
]);
|
|
305
|
+
}),
|
|
306
|
+
Effect.catch((e) =>
|
|
307
|
+
Console.error(` Failed to retrieve credentials: ${e}`),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
import * as Config from "effect/Config";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import { AuthError, getAuthProvider } from "../Auth/AuthProvider.ts";
|
|
8
|
+
import { ALCHEMY_PROFILE, loadOrConfigure } from "../Auth/Profile.ts";
|
|
9
|
+
import {
|
|
10
|
+
GITHUB_AUTH_PROVIDER_NAME,
|
|
11
|
+
type GitHubAuthConfig,
|
|
12
|
+
type GitHubResolvedCredentials,
|
|
13
|
+
} from "./AuthProvider.ts";
|
|
14
|
+
|
|
15
|
+
export interface GitHubCredentialsService {
|
|
16
|
+
readonly token: Redacted.Redacted<string>;
|
|
17
|
+
readonly octokit: () => Octokit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class GitHubCredentials extends Context.Service<
|
|
21
|
+
GitHubCredentials,
|
|
22
|
+
GitHubCredentialsService
|
|
23
|
+
>()("GitHub::Credentials") {}
|
|
24
|
+
|
|
25
|
+
const make = (token: Redacted.Redacted<string>): GitHubCredentialsService => ({
|
|
26
|
+
token,
|
|
27
|
+
octokit: () => new Octokit({ auth: Redacted.value(token) }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a `GitHubCredentials` layer from a literal token. Useful for
|
|
32
|
+
* tests or when callers already have a PAT in hand.
|
|
33
|
+
*/
|
|
34
|
+
export const fromToken = (token: string | Redacted.Redacted<string>) =>
|
|
35
|
+
Layer.succeed(
|
|
36
|
+
GitHubCredentials,
|
|
37
|
+
make(typeof token === "string" ? Redacted.make(token) : token),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a `GitHubCredentials` layer that reads the token from
|
|
42
|
+
* `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` at layer build time.
|
|
43
|
+
*/
|
|
44
|
+
export const fromEnv = () =>
|
|
45
|
+
Layer.effect(
|
|
46
|
+
GitHubCredentials,
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
const access = yield* Config.redacted("GITHUB_ACCESS_TOKEN").pipe(
|
|
49
|
+
Config.option,
|
|
50
|
+
);
|
|
51
|
+
const token = yield* Config.redacted("GITHUB_TOKEN").pipe(Config.option);
|
|
52
|
+
const value =
|
|
53
|
+
access._tag === "Some"
|
|
54
|
+
? access.value
|
|
55
|
+
: token._tag === "Some"
|
|
56
|
+
? token.value
|
|
57
|
+
: undefined;
|
|
58
|
+
if (value == null) {
|
|
59
|
+
return yield* new AuthError({
|
|
60
|
+
message:
|
|
61
|
+
"GitHub credentials not found. Set GITHUB_ACCESS_TOKEN or GITHUB_TOKEN.",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return make(value);
|
|
65
|
+
}).pipe(Effect.orDie),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a `GitHubCredentials` layer that resolves a token via the
|
|
70
|
+
* Alchemy AuthProvider for the configured profile (defaults to
|
|
71
|
+
* `default`, overridable with `ALCHEMY_PROFILE`).
|
|
72
|
+
*/
|
|
73
|
+
export const fromAuthProvider = () =>
|
|
74
|
+
Layer.effect(
|
|
75
|
+
GitHubCredentials,
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const auth = yield* getAuthProvider<
|
|
78
|
+
GitHubAuthConfig,
|
|
79
|
+
GitHubResolvedCredentials
|
|
80
|
+
>(GITHUB_AUTH_PROVIDER_NAME);
|
|
81
|
+
const profileName = yield* ALCHEMY_PROFILE;
|
|
82
|
+
const ci = yield* Config.boolean("CI").pipe(Config.withDefault(false));
|
|
83
|
+
const ctx = yield* Effect.context<never>();
|
|
84
|
+
|
|
85
|
+
return yield* loadOrConfigure(auth, profileName, { ci }).pipe(
|
|
86
|
+
Effect.flatMap((config) =>
|
|
87
|
+
auth.read(profileName, config as GitHubAuthConfig),
|
|
88
|
+
),
|
|
89
|
+
Effect.map((creds) => make(creds.token)),
|
|
90
|
+
Effect.mapError(
|
|
91
|
+
(e) =>
|
|
92
|
+
new AuthError({
|
|
93
|
+
message: `Failed to resolve GitHub credentials for profile '${profileName}': ${(e as { message?: string }).message ?? String(e)}`,
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
Effect.provide(ctx),
|
|
97
|
+
);
|
|
98
|
+
}).pipe(Effect.orDie),
|
|
99
|
+
);
|