@samebase/alchemy-convex 0.5.0 → 0.6.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
5
5
  "keywords": [
6
6
  "alchemy",
@@ -31,8 +31,7 @@
31
31
  ".": {
32
32
  "types": "./lib/index.d.ts",
33
33
  "import": "./lib/index.js"
34
- },
35
- "./convex.config": "./src/component/convex.config.ts"
34
+ }
36
35
  },
37
36
  "publishConfig": {
38
37
  "access": "public"
package/src/Deployment.ts CHANGED
@@ -69,6 +69,7 @@ import {
69
69
  type EnvValue,
70
70
  keyTargetOf,
71
71
  push,
72
+ PUSH_KEY_ACTIONS,
72
73
  pushHash,
73
74
  type Source,
74
75
  withPushKey,
@@ -681,8 +682,14 @@ export const DeploymentProvider = () =>
681
682
  ? push(spawner, access.key, { ...options, previewName: undefined })
682
683
  : access.kind === "previewKey"
683
684
  ? push(spawner, access.key, { ...options, previewName: target.previewName })
684
- : withPushKey(access.api, target.name, news.source, (key) =>
685
- push(spawner, key, { ...options, previewName: undefined }),
685
+ : withPushKey(
686
+ access.api,
687
+ target.name,
688
+ // A push command gets every action; `convex deploy` only its own.
689
+ typeof news.source === "object" && news.source.command !== undefined
690
+ ? undefined
691
+ : PUSH_KEY_ACTIONS,
692
+ (key) => push(spawner, key, { ...options, previewName: undefined }),
686
693
  );
687
694
  yield* pushed.pipe(
688
695
  // A deployment that this reconcile created and could not push to
@@ -69,8 +69,16 @@ export interface ManagementApiService {
69
69
  deployKey: Redacted.Redacted<string>,
70
70
  run: (client: DeploymentClient) => Promise<FetchResult<T>>,
71
71
  ) => Effect.Effect<T, ConvexApiError>;
72
- /** Team id for a slug or numeric id. */
73
- readonly resolveTeamId: (team: string | number) => Effect.Effect<number, ConvexApiError>;
72
+ /**
73
+ * Team id for a numeric id, a slug, or nothing. A team access token names
74
+ * its team (`GET /token_details`); a slug is then checked against the
75
+ * `teamSlug` of the team's projects, so a token of another team fails. The
76
+ * Convex CLI login and a personal token answer 403 there, so a slug goes to
77
+ * the dashboard's team list, and nothing is an error.
78
+ */
79
+ readonly resolveTeamId: (
80
+ team: string | number | undefined,
81
+ ) => Effect.Effect<number, ConvexApiError>;
74
82
  /**
75
83
  * True when a deploy key that Convex returned carries the token of the
76
84
  * Management API credential itself. With an OAuth token, create_deploy_key
@@ -179,6 +187,17 @@ export const absentAsUndefined = <A, R>(
179
187
  const TeamRow = Schema.Struct({ id: Schema.Number, slug: Schema.String });
180
188
  const TeamRows = Schema.Array(TeamRow);
181
189
 
190
+ /** The first page of GET /teams/{team_id}/projects: every row carries the slug of the team. */
191
+ const ProjectSlugRows = Schema.Struct({
192
+ items: Schema.Array(Schema.Struct({ teamSlug: Schema.String })),
193
+ });
194
+
195
+ /** GET /token_details: the team of a team token, or the project of a project token. */
196
+ const TokenDetails = Schema.Union([
197
+ Schema.Struct({ type: Schema.Literal("teamToken"), teamId: Schema.Number }),
198
+ Schema.Struct({ type: Schema.Literal("projectToken"), projectId: Schema.Number }),
199
+ ]);
200
+
182
201
  /** The service for one way to get the bearer token. */
183
202
  const makeService = (credentials: Effect.Effect<ResolvedCredentials>): ManagementApiService => {
184
203
  const client = Effect.cached(
@@ -203,13 +222,76 @@ const makeService = (credentials: Effect.Effect<ResolvedCredentials>): Managemen
203
222
  run(createDeploymentClient(deploymentUrlOrName, Redacted.value(deployKey))),
204
223
  );
205
224
 
225
+ // Measured: a team access token answers `GET /token_details` with its
226
+ // team id and gets HTTP 403 from the dashboard's team list; the Convex CLI
227
+ // login answers the team list and gets HTTP 403 `NotOAuthTeamToken` from
228
+ // token_details. Read once for each service.
229
+ // `Effect.cached` only allocates the cache, so runSync makes one for this service.
230
+ const tokenDetails = Effect.runSync(
231
+ Effect.cached(
232
+ request("get token details", (c) => c.GET("/token_details")).pipe(
233
+ Effect.map((body) => Schema.decodeUnknownSync(TokenDetails)(body)),
234
+ Effect.catchIf(
235
+ (error) => error.status === 403,
236
+ () => Effect.succeed(undefined),
237
+ ),
238
+ ),
239
+ ),
240
+ );
241
+
242
+ /** The slug of the team, from the first project of its listing, or undefined for a team without one. Read once. */
243
+ const teamSlug = (teamId: number) =>
244
+ request("list projects", (c) =>
245
+ c.GET("/teams/{team_id}/projects", { params: { path: { team_id: teamId } } }),
246
+ ).pipe(
247
+ Effect.map((body) => Schema.decodeUnknownSync(ProjectSlugRows)(body).items[0]?.teamSlug),
248
+ );
249
+
206
250
  // The Management API addresses teams by numeric id. The only slug lookup
207
251
  // it offers is per project, so a slug falls back to the dashboard's team
208
252
  // list, which is unofficial. Pass the numeric team id to skip this.
209
253
  const resolveTeamId: ManagementApiService["resolveTeamId"] = (team) =>
210
254
  Effect.gen(function* () {
211
255
  if (typeof team === "number") return team;
212
- if (/^\d+$/.test(team)) return Number(team);
256
+ if (team !== undefined && /^\d+$/.test(team)) return Number(team);
257
+ const details = yield* tokenDetails;
258
+ if (details?.type === "teamToken") {
259
+ if (team === undefined) return details.teamId;
260
+ // The Management API has no team-by-id call, so the slug of the
261
+ // team comes from any of its projects. A team without a project
262
+ // cannot be checked: the numeric id, or no team, is the fix.
263
+ const slug = yield* teamSlug(details.teamId);
264
+ if (slug === undefined) {
265
+ return yield* new ConvexApiError({
266
+ operation: "resolve team",
267
+ status: 0,
268
+ message: `team "${team}" cannot be checked against the access token: team ${details.teamId} has no project yet. Set team to ${details.teamId}, or leave it out.`,
269
+ });
270
+ }
271
+ if (slug !== team) {
272
+ return yield* new ConvexApiError({
273
+ operation: "resolve team",
274
+ status: 403,
275
+ message: `the access token belongs to team ${details.teamId} ("${slug}"), and team is "${team}". Use a token of "${team}", or set team to ${details.teamId}.`,
276
+ });
277
+ }
278
+ return details.teamId;
279
+ }
280
+ if (details?.type === "projectToken") {
281
+ return yield* new ConvexApiError({
282
+ operation: "resolve team",
283
+ status: 403,
284
+ message: `the access token belongs to project ${details.projectId}, and a project token cannot create projects. Use a team access token.`,
285
+ });
286
+ }
287
+ if (team === undefined) {
288
+ return yield* new ConvexApiError({
289
+ operation: "resolve team",
290
+ status: 0,
291
+ message:
292
+ "set team to the slug or the id of the Convex team: this access token does not name a team.",
293
+ });
294
+ }
213
295
  const resolved = yield* credentials;
214
296
  const rows = yield* Effect.tryPromise({
215
297
  try: async () => {
package/src/Project.ts CHANGED
@@ -25,7 +25,12 @@ import * as Effect from "effect/Effect";
25
25
  import * as Schema from "effect/Schema";
26
26
  import type { components } from "@convex-dev/platform/managementApi";
27
27
  import { shouldAdopt } from "./Adoption.ts";
28
- import { absentAsUndefined, ConvexApiError, ManagementApi } from "./ManagementApi.ts";
28
+ import {
29
+ absentAsUndefined,
30
+ ConvexApiError,
31
+ ManagementApi,
32
+ type ManagementApiService,
33
+ } from "./ManagementApi.ts";
29
34
  import type { Providers } from "./Providers.ts";
30
35
 
31
36
  /** Region names the Management API accepts, from the generated schema. */
@@ -159,6 +164,8 @@ const DeploymentRow = Schema.Struct({
159
164
  deploymentType: Schema.String,
160
165
  isDefault: Schema.Boolean,
161
166
  deploymentUrl: Schema.String,
167
+ /** Absent or null until the first push. */
168
+ lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number)),
162
169
  });
163
170
  const DeploymentRows = Schema.Array(DeploymentRow);
164
171
 
@@ -202,29 +209,123 @@ const isTeamOf = (details: ProjectDetails, team: string | number) =>
202
209
  ? details.teamId === Number(team)
203
210
  : details.teamSlug === team;
204
211
 
212
+ /** The row of the project's default production deployment, or undefined. */
213
+ export const prodDeploymentOf = (api: ManagementApiService, projectId: number) =>
214
+ api
215
+ .request("list deployments", (client) =>
216
+ client.GET("/projects/{project_id}/list_deployments", {
217
+ params: { path: { project_id: projectId } },
218
+ }),
219
+ )
220
+ .pipe(
221
+ Effect.map((rows) =>
222
+ Schema.decodeUnknownSync(DeploymentRows)(rows).find(
223
+ (row) => row.deploymentType === "prod" && row.isDefault,
224
+ ),
225
+ ),
226
+ );
227
+
228
+ /** GET /projects/{id}, or undefined after a 404. */
229
+ export const getProjectById = (api: ManagementApiService, projectId: number) =>
230
+ absentAsUndefined(
231
+ api.request("get project", (client) =>
232
+ client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }),
233
+ ),
234
+ ).pipe(
235
+ Effect.map((body) =>
236
+ body === undefined ? undefined : Schema.decodeUnknownSync(ProjectDetails)(body),
237
+ ),
238
+ );
239
+
240
+ /**
241
+ * Every project in the team with exactly this name, in the order of the
242
+ * listing. Reads every page. `q` narrows the listing on the server, and the
243
+ * exact match here decides. Pattern from Confect
244
+ * `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
245
+ * (https://github.com/rjdellecese/confect, ISC license).
246
+ */
247
+ export const listProjectsByName = (api: ManagementApiService, teamId: number, name: string) =>
248
+ Effect.gen(function* () {
249
+ const matches: ProjectDetails[] = [];
250
+ const seenCursors = new Set<string>();
251
+ let cursor: string | undefined;
252
+ while (true) {
253
+ const body = yield* api.request("list projects", (client) =>
254
+ client.GET("/teams/{team_id}/projects", {
255
+ params: {
256
+ path: { team_id: teamId },
257
+ query: { q: name, ...(cursor === undefined ? {} : { cursor }) },
258
+ },
259
+ }),
260
+ );
261
+ const page = Schema.decodeUnknownSync(ProjectPage)(body);
262
+ matches.push(...page.items.filter((row) => row.name === name));
263
+ if (!page.pagination.hasMore) break;
264
+ const next = page.pagination.nextCursor;
265
+ if (next === undefined || next === null || seenCursors.has(next)) {
266
+ return yield* new ConvexApiError({
267
+ operation: "list projects",
268
+ status: 0,
269
+ code: "InvalidPagination",
270
+ message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`,
271
+ });
272
+ }
273
+ seenCursors.add(next);
274
+ cursor = next;
275
+ }
276
+ return matches;
277
+ });
278
+
279
+ /** The one project in the team with exactly this name, or undefined. More than one is an error. */
280
+ export const findProjectByName = (api: ManagementApiService, teamId: number, name: string) =>
281
+ listProjectsByName(api, teamId, name).pipe(
282
+ Effect.flatMap((matches) =>
283
+ matches.length > 1
284
+ ? new AmbiguousProject({ teamId, name, projectIds: matches.map((row) => row.id) })
285
+ : Effect.succeed(matches[0]),
286
+ ),
287
+ );
288
+
289
+ /**
290
+ * POST /teams/{team_id}/create_project, then GET /projects/{id}. The identity
291
+ * comes from the create response, never from a name lookup.
292
+ */
293
+ export const createProject = (
294
+ api: ManagementApiService,
295
+ teamId: number,
296
+ props: {
297
+ readonly name: string;
298
+ readonly deploymentType: "prod" | "dev";
299
+ readonly deploymentRegion: DeploymentRegion | undefined;
300
+ },
301
+ ) =>
302
+ Effect.gen(function* () {
303
+ const created = yield* api.request("create project", (client) =>
304
+ client.POST("/teams/{team_id}/create_project", {
305
+ params: { path: { team_id: teamId } },
306
+ body: {
307
+ projectName: props.name,
308
+ deploymentType: props.deploymentType,
309
+ ...(props.deploymentRegion === undefined
310
+ ? {}
311
+ : { deploymentRegion: props.deploymentRegion }),
312
+ },
313
+ }),
314
+ );
315
+ const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
316
+ const details = yield* getProjectById(api, projectId);
317
+ if (details === undefined) return yield* new CreatedProjectNotFound({ projectId });
318
+ return details;
319
+ });
320
+
205
321
  export const ProjectProvider = () =>
206
322
  Provider.effect(
207
323
  Project,
208
324
  Effect.gen(function* () {
209
325
  const api = yield* ManagementApi;
210
326
 
211
- const prodDeployment = (projectId: number) =>
212
- api
213
- .request("list deployments", (client) =>
214
- client.GET("/projects/{project_id}/list_deployments", {
215
- params: { path: { project_id: projectId } },
216
- }),
217
- )
218
- .pipe(
219
- Effect.map((rows) =>
220
- Schema.decodeUnknownSync(DeploymentRows)(rows).find(
221
- (row) => row.deploymentType === "prod" && row.isDefault,
222
- ),
223
- ),
224
- );
225
-
226
327
  const attributesOf = (details: ProjectDetails) =>
227
- prodDeployment(details.id).pipe(
328
+ prodDeploymentOf(api, details.id).pipe(
228
329
  Effect.map((prod): ProjectAttributes => ({
229
330
  projectId: details.id,
230
331
  slug: details.slug,
@@ -235,16 +336,7 @@ export const ProjectProvider = () =>
235
336
  })),
236
337
  );
237
338
 
238
- const getById = (projectId: number) =>
239
- absentAsUndefined(
240
- api.request("get project", (client) =>
241
- client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }),
242
- ),
243
- ).pipe(
244
- Effect.map((body) =>
245
- body === undefined ? undefined : Schema.decodeUnknownSync(ProjectDetails)(body),
246
- ),
247
- );
339
+ const getById = (projectId: number) => getProjectById(api, projectId);
248
340
 
249
341
  /** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
250
342
  const getBySlug = (team: string | number, slug: string) =>
@@ -260,51 +352,7 @@ export const ProjectProvider = () =>
260
352
  ),
261
353
  );
262
354
 
263
- /**
264
- * The one project in the team with exactly this name, or undefined.
265
- * Reads every page. `q` narrows the listing on the server, and the
266
- * exact match here decides. Pattern from Confect
267
- * `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
268
- * (https://github.com/rjdellecese/confect, ISC license).
269
- */
270
- const findByName = (teamId: number, name: string) =>
271
- Effect.gen(function* () {
272
- const matches: ProjectDetails[] = [];
273
- const seenCursors = new Set<string>();
274
- let cursor: string | undefined;
275
- while (true) {
276
- const body = yield* api.request("list projects", (client) =>
277
- client.GET("/teams/{team_id}/projects", {
278
- params: {
279
- path: { team_id: teamId },
280
- query: { q: name, ...(cursor === undefined ? {} : { cursor }) },
281
- },
282
- }),
283
- );
284
- const page = Schema.decodeUnknownSync(ProjectPage)(body);
285
- matches.push(...page.items.filter((row) => row.name === name));
286
- if (!page.pagination.hasMore) break;
287
- const next = page.pagination.nextCursor;
288
- if (next === undefined || next === null || seenCursors.has(next)) {
289
- return yield* new ConvexApiError({
290
- operation: "list projects",
291
- status: 0,
292
- code: "InvalidPagination",
293
- message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`,
294
- });
295
- }
296
- seenCursors.add(next);
297
- cursor = next;
298
- }
299
- if (matches.length > 1) {
300
- return yield* new AmbiguousProject({
301
- teamId,
302
- name,
303
- projectIds: matches.map((row) => row.id),
304
- });
305
- }
306
- return matches[0];
307
- });
355
+ const findByName = (teamId: number, name: string) => findProjectByName(api, teamId, name);
308
356
 
309
357
  /**
310
358
  * Brings an existing project to the desired name. The team cannot
@@ -458,22 +506,11 @@ export const ProjectProvider = () =>
458
506
  return yield* converge(existing, teamId, news.name);
459
507
  }
460
508
  }
461
- const created = yield* api.request("create project", (client) =>
462
- client.POST("/teams/{team_id}/create_project", {
463
- params: { path: { team_id: teamId } },
464
- body: {
465
- projectName: news.name,
466
- deploymentType: news.deploymentType ?? "prod",
467
- ...(news.deploymentRegion === undefined
468
- ? {}
469
- : { deploymentRegion: news.deploymentRegion }),
470
- },
471
- }),
472
- );
473
- // Identity comes from the create response, never from a name lookup.
474
- const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
475
- const details = yield* getById(projectId);
476
- if (details === undefined) return yield* new CreatedProjectNotFound({ projectId });
509
+ const details = yield* createProject(api, teamId, {
510
+ name: news.name,
511
+ deploymentType: news.deploymentType ?? "prod",
512
+ deploymentRegion: news.deploymentRegion,
513
+ });
477
514
  return yield* converge(details, teamId, news.name);
478
515
  }),
479
516
 
package/src/Push.ts CHANGED
@@ -27,6 +27,7 @@ import { tmpdir } from "node:os";
27
27
  import { dirname, join, relative, resolve } from "node:path";
28
28
  import { stripVTControlCharacters } from "node:util";
29
29
  import { createDeploymentClient } from "@convex-dev/platform";
30
+ import type { paths } from "@convex-dev/platform/managementApi";
30
31
  import { hashDirectory } from "alchemy/Command/Memo";
31
32
  import * as Clock from "effect/Clock";
32
33
  import * as Effect from "effect/Effect";
@@ -80,12 +81,19 @@ const sourceOf = (source: Source) =>
80
81
  /** Lifetime of a push key. Long enough for a slow push with index backfills. */
81
82
  export const PUSH_KEY_LIFETIME_MS = 2 * 60 * 60 * 1000;
82
83
 
83
- /** The only actions a push key gets. */
84
- const PUSH_KEY_ACTIONS = [
84
+ /** An action of a deploy key, such as `deployment:deploy`, from the generated schema. */
85
+ export type DeployKeyAction = NonNullable<
86
+ NonNullable<
87
+ paths["/deployments/{deployment_name}/create_deploy_key"]["post"]["requestBody"]
88
+ >["content"]["application/json"]["allowedActions"]
89
+ >[number];
90
+
91
+ /** The only actions a key for `convex deploy` gets. */
92
+ export const PUSH_KEY_ACTIONS = [
85
93
  "deployment:deploy",
86
94
  "deployment:env:view",
87
95
  "deployment:env:write",
88
- ] as const;
96
+ ] as const satisfies readonly DeployKeyAction[];
89
97
 
90
98
  /** POST /deployments/{deployment_name}/create_deploy_key */
91
99
  const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
@@ -271,7 +279,7 @@ const CliManifest = Schema.Struct({
271
279
  });
272
280
 
273
281
  /** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
274
- const installedCli = (cwd: string) =>
282
+ export const installedCli = (cwd: string) =>
275
283
  Effect.tryPromise({
276
284
  try: async (): Promise<readonly [string, ...string[]]> => {
277
285
  const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
@@ -360,58 +368,78 @@ const cliEnvironment = (home: string): Record<string, string | undefined> => ({
360
368
  XDG_CONFIG_HOME: home,
361
369
  });
362
370
 
371
+ /**
372
+ * A deploy key of `deployment` that expires after PUSH_KEY_LIFETIME_MS, with
373
+ * only `allowedActions`, or every action when that is undefined. Delete it
374
+ * with {@link deleteDeployKey} when the work is done.
375
+ */
376
+ export const createDeployKey = (
377
+ api: ManagementApiService,
378
+ deployment: string,
379
+ name: string,
380
+ allowedActions: readonly DeployKeyAction[] | undefined,
381
+ ) =>
382
+ Effect.gen(function* () {
383
+ const now = yield* Clock.currentTimeMillis;
384
+ const created = yield* api.request("create deploy key", (client) =>
385
+ client.POST("/deployments/{deployment_name}/create_deploy_key", {
386
+ params: { path: { deployment_name: deployment } },
387
+ body: {
388
+ name,
389
+ ...(allowedActions === undefined ? {} : { allowedActions: [...allowedActions] }),
390
+ expiresAt: now + PUSH_KEY_LIFETIME_MS,
391
+ },
392
+ }),
393
+ );
394
+ return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
395
+ });
396
+
397
+ /**
398
+ * Deletes a key that {@link createDeployKey} made. The delete is skipped for
399
+ * a key that is the OAuth credential. A failed delete is logged: the key
400
+ * expires anyway.
401
+ */
402
+ export const deleteDeployKey = (
403
+ api: ManagementApiService,
404
+ deployment: string,
405
+ key: Redacted.Redacted<string>,
406
+ ) =>
407
+ Effect.gen(function* () {
408
+ if (yield* api.isCredentialToken(Redacted.value(key))) return;
409
+ yield* absentAsUndefined(
410
+ retryIdempotentWrite(
411
+ api.requestVoid("delete deploy key", (client) =>
412
+ client.POST("/deployments/{deployment_name}/delete_deploy_key", {
413
+ params: { path: { deployment_name: deployment } },
414
+ body: { id: Redacted.value(key) },
415
+ }),
416
+ ),
417
+ ),
418
+ );
419
+ }).pipe(
420
+ Effect.catch((error) =>
421
+ Effect.logWarning(
422
+ `Convex.Deployment could not delete its key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 60_000} minutes.`,
423
+ ),
424
+ ),
425
+ );
426
+
363
427
  /**
364
428
  * Creates a deploy key for one push, runs `use` with it, and deletes it
365
- * afterwards. Without a push command, the key can only deploy and read and
366
- * write variables. The delete is skipped for a key that is the OAuth credential.
367
- * A failed delete is logged: the key expires anyway.
429
+ * afterwards, also after a failure. `allowedActions` is PUSH_KEY_ACTIONS for
430
+ * `convex deploy`, or undefined for a push command, which can do more than a
431
+ * push, such as the uploads of convex-hosting through `convex run`.
368
432
  */
369
433
  export const withPushKey = <A, E, R>(
370
434
  api: ManagementApiService,
371
435
  deployment: string,
372
- source: Source | undefined,
436
+ allowedActions: readonly DeployKeyAction[] | undefined,
373
437
  use: (key: Redacted.Redacted<string>) => Effect.Effect<A, E, R>,
374
438
  ) =>
375
439
  Effect.acquireUseRelease(
376
- Effect.gen(function* () {
377
- const now = yield* Clock.currentTimeMillis;
378
- const created = yield* api.request("create deploy key", (client) =>
379
- client.POST("/deployments/{deployment_name}/create_deploy_key", {
380
- params: { path: { deployment_name: deployment } },
381
- body: {
382
- name: "alchemy-push",
383
- // A push command can do more than a push, such as the uploads of
384
- // convex-hosting through `convex run`, so its key has every action.
385
- ...(source === undefined || sourceOf(source).command === undefined
386
- ? { allowedActions: [...PUSH_KEY_ACTIONS] }
387
- : {}),
388
- expiresAt: now + PUSH_KEY_LIFETIME_MS,
389
- },
390
- }),
391
- );
392
- return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
393
- }),
440
+ createDeployKey(api, deployment, "alchemy-push", allowedActions),
394
441
  use,
395
- (key) =>
396
- Effect.gen(function* () {
397
- if (yield* api.isCredentialToken(Redacted.value(key))) return;
398
- yield* absentAsUndefined(
399
- retryIdempotentWrite(
400
- api.requestVoid("delete deploy key", (client) =>
401
- client.POST("/deployments/{deployment_name}/delete_deploy_key", {
402
- params: { path: { deployment_name: deployment } },
403
- body: { id: Redacted.value(key) },
404
- }),
405
- ),
406
- ),
407
- );
408
- }).pipe(
409
- Effect.catch((error) =>
410
- Effect.logWarning(
411
- `Convex.Deployment could not delete its push key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 60_000} minutes.`,
412
- ),
413
- ),
414
- ),
442
+ (key) => deleteDeployKey(api, deployment, key),
415
443
  );
416
444
 
417
445
  /** Every variable of the deployment with its value. */