@samebase/alchemy-convex 0.2.0 → 0.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.2.0",
4
- "description": "Convex resources for Alchemy v2: projects, deploy keys, environment variables, and a deploy step",
3
+ "version": "0.4.0",
4
+ "description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
5
5
  "keywords": [
6
6
  "alchemy",
7
7
  "cloudflare",
@@ -22,7 +22,8 @@
22
22
  "lib",
23
23
  "src",
24
24
  "README.md",
25
- "LICENSE"
25
+ "LICENSE",
26
+ "NOTICE"
26
27
  ],
27
28
  "type": "module",
28
29
  "sideEffects": false,
@@ -1,9 +1,20 @@
1
- // Convex control-plane credentials for the provider.
1
+ // Convex credentials for the provider, by scope.
2
2
  //
3
- // The service value is an Effect so that building the providers layer never
4
- // touches the environment or the filesystem. The token is read on the first
5
- // lifecycle call, which keeps `alchemy plan` on an unrelated stack free of
6
- // Convex credential errors.
3
+ // - The access token works on the Management API: a team access token, an
4
+ // OAuth token, or the Convex CLI login. Convex.Project needs it. A
5
+ // Convex.Deployment needs it only when the deploy key does not fit.
6
+ // - The deploy key is optional. A deploy key of one deployment
7
+ // (`prod:<name>|...`, `dev:<name>|...`) lets Convex.Deployment set
8
+ // variables and push to that deployment with no access token. A preview
9
+ // deploy key of a project (`preview:<team>:<project>|...`) lets it create,
10
+ // push to, and delete preview deployments of that project with no access
11
+ // token.
12
+ //
13
+ // Credentials are a layer, never props: they do not go into Alchemy state.
14
+ // Both values are Effects, so that building the providers layer never touches
15
+ // the environment or the filesystem. A value is read on the first call that
16
+ // needs it, which keeps `alchemy plan` on an unrelated stack free of Convex
17
+ // credential errors.
7
18
  import { readFile } from "node:fs/promises";
8
19
  import { homedir } from "node:os";
9
20
  import { join } from "node:path";
@@ -11,7 +22,9 @@ import * as Config from "effect/Config";
11
22
  import * as Context from "effect/Context";
12
23
  import * as Effect from "effect/Effect";
13
24
  import * as Layer from "effect/Layer";
25
+ import * as Option from "effect/Option";
14
26
  import * as Redacted from "effect/Redacted";
27
+ import * as Result from "effect/Result";
15
28
  import * as Schema from "effect/Schema";
16
29
 
17
30
  export interface ResolvedCredentials {
@@ -21,18 +34,34 @@ export interface ResolvedCredentials {
21
34
  readonly source: "env" | "explicit" | "convex-cli-login";
22
35
  }
23
36
 
24
- export class Credentials extends Context.Service<Credentials, Effect.Effect<ResolvedCredentials>>()(
37
+ export interface CredentialsService {
38
+ /** The Management API token. Dies when there is none. */
39
+ readonly accessToken: Effect.Effect<ResolvedCredentials>;
40
+ /** True when there is a Management API token. */
41
+ readonly hasAccessToken: Effect.Effect<boolean>;
42
+ /** A deploy key or a preview deploy key, or undefined. */
43
+ readonly deployKey: Effect.Effect<Redacted.Redacted<string> | undefined>;
44
+ }
45
+
46
+ export class Credentials extends Context.Service<Credentials, CredentialsService>()(
25
47
  "@samebase/alchemy-convex/Credentials",
26
48
  ) {}
27
49
 
28
- /** Environment variable read by {@link fromConfig}. */
50
+ /** Environment variable for the access token, read by {@link fromConfig}. */
29
51
  export const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
30
52
 
53
+ /** Environment variable for the deploy key, read by {@link fromConfig}. The Convex CLI reads the same name. */
54
+ export const DEPLOY_KEY_ENV = "CONVEX_DEPLOY_KEY";
55
+
31
56
  /** The Convex CLI stores its device login here after `npx convex login`. */
32
57
  export const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
33
58
 
34
59
  const CliConfig = Schema.Struct({ accessToken: Schema.String });
35
60
 
61
+ export class CredentialsError extends Schema.TaggedError<CredentialsError>()("CredentialsError", {
62
+ reason: Schema.String,
63
+ }) {}
64
+
36
65
  /**
37
66
  * Token the Convex CLI saved on this machine. It works on the Management API
38
67
  * for the teams the logged-in user belongs to. Fails when nobody is logged in.
@@ -42,7 +71,10 @@ export const readConvexCliLogin: Effect.Effect<ResolvedCredentials, CredentialsE
42
71
  const path = convexCliConfigPath();
43
72
  const text = yield* Effect.tryPromise({
44
73
  try: () => readFile(path, "utf8"),
45
- catch: () => new CredentialsError({ reason: `no Convex CLI login at ${path}` }),
74
+ catch: () =>
75
+ new CredentialsError({
76
+ reason: `This step needs the Convex Management API. Set ${ACCESS_TOKEN_ENV} to a team access token, or run \`npx convex login\`. There is no Convex CLI login at ${path}.`,
77
+ }),
46
78
  });
47
79
  const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(
48
80
  Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })),
@@ -51,28 +83,54 @@ export const readConvexCliLogin: Effect.Effect<ResolvedCredentials, CredentialsE
51
83
  },
52
84
  );
53
85
 
54
- export class CredentialsError extends Schema.TaggedError<CredentialsError>()("CredentialsError", {
55
- reason: Schema.String,
56
- }) {}
57
-
58
86
  /**
59
- * Default resolution: `CONVEX_ACCESS_TOKEN` from the stack ConfigProvider
60
- * (process env, `.env`, stack secrets), then the Convex CLI login on this
61
- * machine. CI sets the variable; a developer who ran `npx convex login` needs
62
- * nothing.
87
+ * Default resolution through the stack ConfigProvider (process env, `.env`,
88
+ * stack secrets): `CONVEX_ACCESS_TOKEN`, then the Convex CLI login on this
89
+ * machine, for the access token; `CONVEX_DEPLOY_KEY` for the deploy key. A
90
+ * developer who ran `npx convex login` needs nothing. CI sets only the
91
+ * deploy key when the stack pushes to deployments that exist, and the access
92
+ * token when the stack also creates projects or deployments.
63
93
  */
64
94
  export const fromConfig = (): Layer.Layer<Credentials> =>
65
- Layer.succeed(
95
+ Layer.effect(
66
96
  Credentials,
67
- Effect.cached(
68
- Config.Redacted(ACCESS_TOKEN_ENV).pipe(
69
- Effect.map((accessToken): ResolvedCredentials => ({ accessToken, source: "env" })),
70
- Effect.catch(() => readConvexCliLogin),
71
- Effect.orDie,
72
- ),
73
- ).pipe(Effect.flatten),
97
+ Effect.gen(function* () {
98
+ const resolved = yield* Effect.cached(
99
+ Config.Redacted(ACCESS_TOKEN_ENV).pipe(
100
+ Effect.map((token): ResolvedCredentials => ({ accessToken: token, source: "env" })),
101
+ Effect.catch(() => readConvexCliLogin),
102
+ Effect.result,
103
+ ),
104
+ );
105
+ const deployKey = yield* Effect.cached(
106
+ Config.option(Config.Redacted(DEPLOY_KEY_ENV)).pipe(
107
+ Effect.map(Option.getOrUndefined),
108
+ Effect.orDie,
109
+ ),
110
+ );
111
+ return {
112
+ accessToken: resolved.pipe(Effect.flatMap(Effect.fromResult), Effect.orDie),
113
+ hasAccessToken: resolved.pipe(Effect.map(Result.isSuccess)),
114
+ deployKey,
115
+ };
116
+ }),
74
117
  );
75
118
 
76
- /** Explicit token, for tests or callers that already hold a value. */
77
- export const fromToken = (accessToken: Redacted.Redacted<string>): Layer.Layer<Credentials> =>
78
- Layer.succeed(Credentials, Effect.succeed({ accessToken, source: "explicit" } as const));
119
+ /** Explicit values, for tests or callers that already hold them. */
120
+ export const fromValues = (values: {
121
+ readonly accessToken?: Redacted.Redacted<string>;
122
+ readonly deployKey?: Redacted.Redacted<string>;
123
+ }): Layer.Layer<Credentials> =>
124
+ Layer.succeed(Credentials, {
125
+ accessToken:
126
+ values.accessToken === undefined
127
+ ? Effect.die(
128
+ new CredentialsError({
129
+ reason:
130
+ "This step needs the Convex Management API, and fromValues got no accessToken.",
131
+ }),
132
+ )
133
+ : Effect.succeed({ accessToken: values.accessToken, source: "explicit" } as const),
134
+ hasAccessToken: Effect.succeed(values.accessToken !== undefined),
135
+ deployKey: Effect.succeed(values.deployKey),
136
+ });