alchemy 2.0.0-beta.15 → 2.0.0-beta.17
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 +1661 -710
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/cli.js +9 -7
- package/bin/commands/deploy.ts +11 -2
- package/bin/commands/login.ts +6 -3
- package/bin/commands/profile.ts +106 -0
- package/bin/commands/state.ts +91 -1
- package/lib/Auth/Credentials.d.ts +6 -0
- package/lib/Auth/Credentials.d.ts.map +1 -1
- package/lib/Auth/Credentials.js +12 -1
- package/lib/Auth/Credentials.js.map +1 -1
- package/lib/Auth/Profile.d.ts +5 -0
- package/lib/Auth/Profile.d.ts.map +1 -1
- package/lib/Auth/Profile.js +11 -0
- package/lib/Auth/Profile.js.map +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +4 -4
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.js +16 -11
- package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -1
- package/lib/Cloudflare/ApiToken/Common.d.ts +11 -12
- package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/Common.js +4 -14
- 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/Comment.d.ts +2 -1
- package/lib/GitHub/Comment.d.ts.map +1 -1
- package/lib/GitHub/Comment.js +1 -0
- package/lib/GitHub/Comment.js.map +1 -1
- 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 +14 -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/Auth/Credentials.ts +17 -1
- package/src/Auth/Profile.ts +15 -0
- package/src/Cloudflare/ApiToken/AccountApiToken.ts +25 -14
- package/src/Cloudflare/ApiToken/Common.ts +12 -19
- 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/Comment.ts +4 -1
- package/src/GitHub/Credentials.ts +99 -0
- package/src/GitHub/Providers.ts +29 -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,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
|
+
);
|
package/src/GitHub/Comment.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";
|
|
|
3
3
|
import * as Provider from "../Provider.ts";
|
|
4
4
|
import { Resource } from "../Resource.ts";
|
|
5
5
|
import { dedent } from "../Util/dedent.ts";
|
|
6
|
+
import * as GitHub from "./Providers.ts";
|
|
6
7
|
|
|
7
8
|
export interface CommentProps {
|
|
8
9
|
/**
|
|
@@ -62,7 +63,9 @@ export interface Comment extends Resource<
|
|
|
62
63
|
* ISO-8601 timestamp of the last update.
|
|
63
64
|
*/
|
|
64
65
|
updatedAt: string;
|
|
65
|
-
}
|
|
66
|
+
},
|
|
67
|
+
never,
|
|
68
|
+
GitHub.Providers
|
|
66
69
|
> {}
|
|
67
70
|
|
|
68
71
|
/**
|
|
@@ -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
|
+
);
|
package/src/GitHub/Providers.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import * as Layer from "effect/Layer";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import * as Provider from "../Provider.ts";
|
|
3
|
+
import { GitHubAuth } from "./AuthProvider.ts";
|
|
4
|
+
import { Comment, CommentProvider } from "./Comment.ts";
|
|
5
|
+
import * as Credentials from "./Credentials.ts";
|
|
6
|
+
import { Secret, SecretProvider } from "./Secret.ts";
|
|
7
|
+
import { Variable, VariableProvider } from "./Variable.ts";
|
|
4
8
|
|
|
9
|
+
export { GitHubCredentials } from "./Credentials.ts";
|
|
10
|
+
|
|
11
|
+
export class Providers extends Provider.ProviderCollection<Providers>()(
|
|
12
|
+
"GitHub",
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GitHub providers (Secret, Variable) plus the GitHub AuthProvider that
|
|
19
|
+
* `alchemy login` discovers.
|
|
20
|
+
*/
|
|
5
21
|
export const providers = () =>
|
|
6
|
-
Layer.
|
|
22
|
+
Layer.effect(
|
|
23
|
+
Providers,
|
|
24
|
+
Provider.collection([Comment, Secret, Variable]),
|
|
25
|
+
).pipe(
|
|
26
|
+
Layer.provide(
|
|
27
|
+
Layer.mergeAll(CommentProvider(), SecretProvider(), VariableProvider()),
|
|
28
|
+
),
|
|
29
|
+
Layer.provideMerge(Credentials.fromAuthProvider()),
|
|
30
|
+
Layer.provideMerge(GitHubAuth),
|
|
31
|
+
Layer.orDie,
|
|
32
|
+
);
|
package/src/GitHub/Secret.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Octokit } from "@octokit/rest";
|
|
1
|
+
import type { Octokit } from "@octokit/rest";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Redacted from "effect/Redacted";
|
|
4
4
|
import * as Provider from "../Provider.ts";
|
|
5
5
|
import { Resource } from "../Resource.ts";
|
|
6
|
+
import { GitHubCredentials } from "./Credentials.ts";
|
|
7
|
+
import type * as GitHub from "./Providers.ts";
|
|
6
8
|
|
|
7
9
|
export interface SecretProps {
|
|
8
10
|
/**
|
|
@@ -31,12 +33,6 @@ export interface SecretProps {
|
|
|
31
33
|
* GitHub Actions environment instead of the whole repository.
|
|
32
34
|
*/
|
|
33
35
|
environment?: string;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* GitHub API token. If not provided, falls back to
|
|
37
|
-
* `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
|
|
38
|
-
*/
|
|
39
|
-
token?: string;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
export interface Secret extends Resource<
|
|
@@ -47,7 +43,9 @@ export interface Secret extends Resource<
|
|
|
47
43
|
* ISO-8601 timestamp of the last update.
|
|
48
44
|
*/
|
|
49
45
|
updatedAt: string;
|
|
50
|
-
}
|
|
46
|
+
},
|
|
47
|
+
never,
|
|
48
|
+
GitHub.Providers
|
|
51
49
|
> {}
|
|
52
50
|
|
|
53
51
|
/**
|
|
@@ -58,9 +56,10 @@ export interface Secret extends Resource<
|
|
|
58
56
|
* key via `libsodium` before being stored. The resource is idempotent —
|
|
59
57
|
* calling it with the same name will update the secret value in place.
|
|
60
58
|
*
|
|
61
|
-
* Authentication is resolved
|
|
62
|
-
* `
|
|
63
|
-
* `
|
|
59
|
+
* Authentication is resolved via the `GitHubCredentials` service supplied
|
|
60
|
+
* by `GitHub.providers()` (which uses the Alchemy AuthProvider — env,
|
|
61
|
+
* stored PAT, `gh` CLI, or OAuth). The token needs `repo` scope for
|
|
62
|
+
* private repositories or `public_repo` for public ones.
|
|
64
63
|
*
|
|
65
64
|
* @section Repository Secrets
|
|
66
65
|
* Store secrets accessible to all GitHub Actions workflows in the
|
|
@@ -130,21 +129,20 @@ export interface Secret extends Resource<
|
|
|
130
129
|
*/
|
|
131
130
|
export const Secret = Resource<Secret>("GitHub.Secret");
|
|
132
131
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function createClient(props: SecretProps): Octokit {
|
|
140
|
-
return new Octokit({ auth: resolveToken(props) });
|
|
141
|
-
}
|
|
132
|
+
const getOctokit = Effect.gen(function* () {
|
|
133
|
+
const creds = yield* GitHubCredentials;
|
|
134
|
+
return creds.octokit();
|
|
135
|
+
});
|
|
142
136
|
|
|
143
137
|
async function encryptValue(
|
|
144
138
|
plaintext: string,
|
|
145
139
|
publicKey: string,
|
|
146
140
|
): Promise<string> {
|
|
147
|
-
const
|
|
141
|
+
const mod = await import("libsodium-wrappers");
|
|
142
|
+
// Bun/ESM interop: the actual sodium API lives on `.default` when the
|
|
143
|
+
// CJS module is wrapped, but is the module itself under other loaders.
|
|
144
|
+
const sodium: typeof import("libsodium-wrappers") =
|
|
145
|
+
(mod as any).default ?? mod;
|
|
148
146
|
await sodium.ready;
|
|
149
147
|
const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
|
|
150
148
|
const binMessage = sodium.from_string(plaintext);
|
|
@@ -155,13 +153,13 @@ async function encryptValue(
|
|
|
155
153
|
export const SecretProvider = () =>
|
|
156
154
|
Provider.succeed(Secret, {
|
|
157
155
|
create: Effect.fn(function* ({ news }) {
|
|
158
|
-
const octokit =
|
|
156
|
+
const octokit = yield* getOctokit;
|
|
159
157
|
yield* upsertSecret(octokit, news);
|
|
160
158
|
return { updatedAt: new Date().toISOString() };
|
|
161
159
|
}),
|
|
162
160
|
|
|
163
161
|
update: Effect.fn(function* ({ news, olds }) {
|
|
164
|
-
const octokit =
|
|
162
|
+
const octokit = yield* getOctokit;
|
|
165
163
|
|
|
166
164
|
const wasEnv = !!olds.environment;
|
|
167
165
|
const isEnv = !!news.environment;
|
|
@@ -174,7 +172,7 @@ export const SecretProvider = () =>
|
|
|
174
172
|
}),
|
|
175
173
|
|
|
176
174
|
delete: Effect.fn(function* ({ olds }) {
|
|
177
|
-
const octokit =
|
|
175
|
+
const octokit = yield* getOctokit;
|
|
178
176
|
yield* deleteSecret(octokit, olds);
|
|
179
177
|
}),
|
|
180
178
|
});
|
package/src/GitHub/Variable.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Octokit } from "@octokit/rest";
|
|
2
1
|
import * as Effect from "effect/Effect";
|
|
3
2
|
import * as Provider from "../Provider.ts";
|
|
4
3
|
import { Resource } from "../Resource.ts";
|
|
4
|
+
import { GitHubCredentials } from "./Credentials.ts";
|
|
5
|
+
import type * as GitHub from "./Providers.ts";
|
|
5
6
|
|
|
6
7
|
export interface VariableProps {
|
|
7
8
|
/**
|
|
@@ -23,12 +24,6 @@ export interface VariableProps {
|
|
|
23
24
|
* Variable value.
|
|
24
25
|
*/
|
|
25
26
|
value: string;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* GitHub API token. If not provided, falls back to
|
|
29
|
-
* `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
|
|
30
|
-
*/
|
|
31
|
-
token?: string;
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
export interface Variable extends Resource<
|
|
@@ -39,7 +34,9 @@ export interface Variable extends Resource<
|
|
|
39
34
|
* ISO-8601 timestamp of the last update.
|
|
40
35
|
*/
|
|
41
36
|
updatedAt: string;
|
|
42
|
-
}
|
|
37
|
+
},
|
|
38
|
+
never,
|
|
39
|
+
GitHub.Providers
|
|
43
40
|
> {}
|
|
44
41
|
|
|
45
42
|
/**
|
|
@@ -51,9 +48,10 @@ export interface Variable extends Resource<
|
|
|
51
48
|
* labels, or feature flags. For sensitive values, use `GitHub.Secret`
|
|
52
49
|
* instead.
|
|
53
50
|
*
|
|
54
|
-
* Authentication is resolved
|
|
55
|
-
* `
|
|
56
|
-
* `
|
|
51
|
+
* Authentication is resolved via the `GitHubCredentials` service supplied
|
|
52
|
+
* by `GitHub.providers()` (which uses the Alchemy AuthProvider — env,
|
|
53
|
+
* stored PAT, `gh` CLI, or OAuth). The token needs `repo` scope for
|
|
54
|
+
* private repositories or `public_repo` for public ones.
|
|
57
55
|
*
|
|
58
56
|
* @section Repository Variables
|
|
59
57
|
* Store variables accessible to all GitHub Actions workflows in the
|
|
@@ -104,20 +102,15 @@ export interface Variable extends Resource<
|
|
|
104
102
|
*/
|
|
105
103
|
export const Variable = Resource<Variable>("GitHub.Variable");
|
|
106
104
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function createClient(props: VariableProps): Octokit {
|
|
114
|
-
return new Octokit({ auth: resolveToken(props) });
|
|
115
|
-
}
|
|
105
|
+
const getOctokit = Effect.gen(function* () {
|
|
106
|
+
const creds = yield* GitHubCredentials;
|
|
107
|
+
return creds.octokit();
|
|
108
|
+
});
|
|
116
109
|
|
|
117
110
|
export const VariableProvider = () =>
|
|
118
111
|
Provider.succeed(Variable, {
|
|
119
112
|
create: Effect.fn(function* ({ news }) {
|
|
120
|
-
const octokit =
|
|
113
|
+
const octokit = yield* getOctokit;
|
|
121
114
|
|
|
122
115
|
yield* Effect.tryPromise(() =>
|
|
123
116
|
octokit.rest.actions.createRepoVariable({
|
|
@@ -132,7 +125,7 @@ export const VariableProvider = () =>
|
|
|
132
125
|
}),
|
|
133
126
|
|
|
134
127
|
update: Effect.fn(function* ({ news }) {
|
|
135
|
-
const octokit =
|
|
128
|
+
const octokit = yield* getOctokit;
|
|
136
129
|
|
|
137
130
|
yield* Effect.tryPromise(() =>
|
|
138
131
|
octokit.rest.actions.updateRepoVariable({
|
|
@@ -147,7 +140,7 @@ export const VariableProvider = () =>
|
|
|
147
140
|
}),
|
|
148
141
|
|
|
149
142
|
delete: Effect.fn(function* ({ olds }) {
|
|
150
|
-
const octokit =
|
|
143
|
+
const octokit = yield* getOctokit;
|
|
151
144
|
|
|
152
145
|
yield* Effect.tryPromise(async () => {
|
|
153
146
|
try {
|
package/src/GitHub/index.ts
CHANGED