@samebase/alchemy-convex 0.1.0 → 0.2.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/README.md +85 -17
- package/lib/index.d.ts +352 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +962 -10
- package/lib/index.js.map +1 -1
- package/package.json +7 -4
- package/src/Adoption.ts +30 -0
- package/src/DefaultEnvironmentVariable.ts +127 -29
- package/src/Deploy.ts +37 -15
- package/src/DeployKey.ts +221 -60
- package/src/EnvironmentVariable.ts +174 -41
- package/src/ManagementApi.ts +57 -1
- package/src/PreviewDeployKey.ts +65 -38
- package/src/Project.ts +236 -52
- package/src/index.ts +9 -0
- package/lib/Credentials.d.ts +0 -39
- package/lib/Credentials.d.ts.map +0 -1
- package/lib/Credentials.js +0 -49
- package/lib/Credentials.js.map +0 -1
- package/lib/DefaultEnvironmentVariable.d.ts +0 -58
- package/lib/DefaultEnvironmentVariable.d.ts.map +0 -1
- package/lib/DefaultEnvironmentVariable.js +0 -93
- package/lib/DefaultEnvironmentVariable.js.map +0 -1
- package/lib/Deploy.d.ts +0 -62
- package/lib/Deploy.d.ts.map +0 -1
- package/lib/Deploy.js +0 -92
- package/lib/Deploy.js.map +0 -1
- package/lib/DeployKey.d.ts +0 -48
- package/lib/DeployKey.d.ts.map +0 -1
- package/lib/DeployKey.js +0 -104
- package/lib/DeployKey.js.map +0 -1
- package/lib/EnvironmentVariable.d.ts +0 -37
- package/lib/EnvironmentVariable.d.ts.map +0 -1
- package/lib/EnvironmentVariable.js +0 -65
- package/lib/EnvironmentVariable.js.map +0 -1
- package/lib/ManagementApi.d.ts +0 -50
- package/lib/ManagementApi.d.ts.map +0 -1
- package/lib/ManagementApi.js +0 -103
- package/lib/ManagementApi.js.map +0 -1
- package/lib/PreviewDeployKey.d.ts +0 -34
- package/lib/PreviewDeployKey.d.ts.map +0 -1
- package/lib/PreviewDeployKey.js +0 -78
- package/lib/PreviewDeployKey.js.map +0 -1
- package/lib/Project.d.ts +0 -29
- package/lib/Project.d.ts.map +0 -1
- package/lib/Project.js +0 -114
- package/lib/Project.js.map +0 -1
- package/lib/Providers.d.ts +0 -13
- package/lib/Providers.d.ts.map +0 -1
- package/lib/Providers.js +0 -30
- package/lib/Providers.js.map +0 -1
package/src/Deploy.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { havePropsChanged, isResolved } from "alchemy/Diff";
|
|
|
12
12
|
import * as Provider from "alchemy/Provider";
|
|
13
13
|
import * as Effect from "effect/Effect";
|
|
14
14
|
import type * as Redacted from "effect/Redacted";
|
|
15
|
+
import * as Schema from "effect/Schema";
|
|
15
16
|
import type { Providers } from "./Providers.ts";
|
|
16
17
|
|
|
17
18
|
export interface DeployProps extends Pick<CommandRunProps, "cwd" | "env" | "timeout"> {
|
|
@@ -46,7 +47,9 @@ export interface DeployProps extends Pick<CommandRunProps, "cwd" | "env" | "time
|
|
|
46
47
|
* Files hashed to skip an unchanged push. By default every non-gitignored
|
|
47
48
|
* file under `cwd` and the nearest lockfile. Use
|
|
48
49
|
* `{ include: ["convex/**"], lockfile: true }` to ignore frontend changes,
|
|
49
|
-
* or `false` to push on every deploy.
|
|
50
|
+
* or `false` to push on every deploy. A preview deploy (`previewName` or
|
|
51
|
+
* `previewCreate`) pushes on every deploy: Convex can delete an expired
|
|
52
|
+
* preview deployment, and the hash cannot show that.
|
|
50
53
|
* @default true
|
|
51
54
|
*/
|
|
52
55
|
readonly memo?: MemoOptions | boolean;
|
|
@@ -99,6 +102,26 @@ export const deployArgs = (
|
|
|
99
102
|
/** Number of output lines quoted when the CLI exits 0 without the deployed line. */
|
|
100
103
|
const OUTPUT_TAIL_LINES = 20;
|
|
101
104
|
|
|
105
|
+
/** A `convex deploy` argument contains whitespace, which Alchemy's command runner splits on. */
|
|
106
|
+
export class DeployArgumentError extends Schema.TaggedError<DeployArgumentError>()(
|
|
107
|
+
"DeployArgumentError",
|
|
108
|
+
{ argument: Schema.String },
|
|
109
|
+
) {
|
|
110
|
+
override get message() {
|
|
111
|
+
return `Convex.Deploy argument "${this.argument}" contains whitespace, and Alchemy splits the command on whitespace. Use the --flag=value form, such as --typecheck=disable, and names without spaces.`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `npx convex deploy` exited 0 but did not print the deployment URL. */
|
|
116
|
+
export class DeployOutputError extends Schema.TaggedError<DeployOutputError>()(
|
|
117
|
+
"DeployOutputError",
|
|
118
|
+
{ outputTail: Schema.String },
|
|
119
|
+
) {
|
|
120
|
+
override get message() {
|
|
121
|
+
return `npx convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line, so Convex.Deploy has no deployment URL. Remove --dry-run from extraArgs if it is there, and read the CLI output. Last output:\n${this.outputTail}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
export const DeployProvider = () =>
|
|
103
126
|
Provider.effect(
|
|
104
127
|
Deploy,
|
|
@@ -120,7 +143,16 @@ export const DeployProvider = () =>
|
|
|
120
143
|
// under the same name.
|
|
121
144
|
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
122
145
|
if (!isResolved(news)) return undefined;
|
|
123
|
-
|
|
146
|
+
// A preview deployment can expire. The same hash then hides a
|
|
147
|
+
// missing deployment, so a preview always pushes. The CLI reuses
|
|
148
|
+
// a live preview with the same name.
|
|
149
|
+
const preview = news.previewName !== undefined || news.previewCreate !== undefined;
|
|
150
|
+
if (
|
|
151
|
+
preview ||
|
|
152
|
+
output === undefined ||
|
|
153
|
+
output.hash === undefined ||
|
|
154
|
+
havePropsChanged(olds, news)
|
|
155
|
+
) {
|
|
124
156
|
return { action: "update" } as const;
|
|
125
157
|
}
|
|
126
158
|
return { action: (yield* hashOf(news)) === output.hash ? "noop" : "update" } as const;
|
|
@@ -129,13 +161,7 @@ export const DeployProvider = () =>
|
|
|
129
161
|
reconcile: Effect.fn(function* ({ news, session }) {
|
|
130
162
|
const args = deployArgs(news);
|
|
131
163
|
const spaced = args.find((arg) => /\s/.test(arg));
|
|
132
|
-
if (spaced !== undefined) {
|
|
133
|
-
return yield* Effect.die(
|
|
134
|
-
new Error(
|
|
135
|
-
`Convex.Deploy argument "${spaced}" contains whitespace; Alchemy splits commands on whitespace`,
|
|
136
|
-
),
|
|
137
|
-
);
|
|
138
|
-
}
|
|
164
|
+
if (spaced !== undefined) return yield* new DeployArgumentError({ argument: spaced });
|
|
139
165
|
// A push is idempotent: the CLI uploads the full function set every time.
|
|
140
166
|
const { stdout, stderr } = yield* run(
|
|
141
167
|
{
|
|
@@ -150,16 +176,12 @@ export const DeployProvider = () =>
|
|
|
150
176
|
const output = `${stdout}\n${stderr}`;
|
|
151
177
|
const deployed = parseDeployOutput(output);
|
|
152
178
|
if (deployed === undefined) {
|
|
153
|
-
const
|
|
179
|
+
const outputTail = stripVTControlCharacters(output)
|
|
154
180
|
.split(/\r?\n|\r/)
|
|
155
181
|
.filter((line) => line.trim() !== "")
|
|
156
182
|
.slice(-OUTPUT_TAIL_LINES)
|
|
157
183
|
.join("\n");
|
|
158
|
-
return yield*
|
|
159
|
-
new Error(
|
|
160
|
-
`npx convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line. Last output:\n${tail}`,
|
|
161
|
-
),
|
|
162
|
-
);
|
|
184
|
+
return yield* new DeployOutputError({ outputTail });
|
|
163
185
|
}
|
|
164
186
|
return { ...deployed, hash: yield* hashOf(news) };
|
|
165
187
|
}),
|
package/src/DeployKey.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
// Convex.DeployKey: a deploy key for one deployment, such as the key
|
|
2
2
|
// `npx convex deploy` reads from CONVEX_DEPLOY_KEY.
|
|
3
3
|
//
|
|
4
|
-
// Convex returns the secret only once, from create_deploy_key
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Convex returns the secret only once, from create_deploy_key, and the
|
|
5
|
+
// response has no id. The listing shows each key with a numeric id and a
|
|
6
|
+
// name: the requested name, or, when a listed key already has that name, the
|
|
7
|
+
// requested name plus a suffix such as " (870993b4-ffe6-4911-adbd-e29f7fd712f2)".
|
|
8
|
+
//
|
|
9
|
+
// One resource must never revoke the key of another resource:
|
|
10
|
+
// - The requested name is the `name` prop plus a hash of the resource's fqn
|
|
11
|
+
// and instance id, so two resources with the same `name` request
|
|
12
|
+
// different names.
|
|
13
|
+
// - After the create, exactly one listed key must have the requested name.
|
|
14
|
+
// Otherwise the create fails with a typed error that lists the keys. State
|
|
15
|
+
// keeps that key's numeric id.
|
|
16
|
+
// - Delete sends the secret as `id`, so it can revoke only the key that this
|
|
17
|
+
// resource created. It never deletes by a listed name.
|
|
18
|
+
// - With an OAuth token as the Management API credential, Convex returns
|
|
19
|
+
// that token as the "new" key. The provider never stores, revokes, or
|
|
20
|
+
// deletes such a key: the create fails with DeployKeyIsCredential.
|
|
21
|
+
// - Delete runs only for a key with a `keyId` in state. A 0.1.x key, which
|
|
22
|
+
// has none, stays in Convex with a warning.
|
|
23
|
+
// A key cannot change after creation, so every property change is a replacement.
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
9
25
|
import { Resource } from "alchemy";
|
|
10
26
|
import { havePropsChanged, isResolved } from "alchemy/Diff";
|
|
11
27
|
import * as Provider from "alchemy/Provider";
|
|
@@ -13,7 +29,12 @@ import * as Effect from "effect/Effect";
|
|
|
13
29
|
import * as Redacted from "effect/Redacted";
|
|
14
30
|
import * as Schema from "effect/Schema";
|
|
15
31
|
import type { components } from "@convex-dev/platform/managementApi";
|
|
16
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
absentAsUndefined,
|
|
34
|
+
type ConvexApiError,
|
|
35
|
+
ManagementApi,
|
|
36
|
+
retryIdempotentWrite,
|
|
37
|
+
} from "./ManagementApi.ts";
|
|
17
38
|
import type { Providers } from "./Providers.ts";
|
|
18
39
|
|
|
19
40
|
/** Actions a deploy key can be limited to, from the generated schema. */
|
|
@@ -23,35 +44,50 @@ export type DeployKeyAction = NonNullable<
|
|
|
23
44
|
|
|
24
45
|
/**
|
|
25
46
|
* True when `listedName` is how Convex lists a key created as `name`: the
|
|
26
|
-
* requested name, a space, and one
|
|
27
|
-
*
|
|
47
|
+
* requested name itself, or the requested name, a space, and one
|
|
48
|
+
* parenthesized suffix without spaces. The suffix check keeps `my-key` from
|
|
49
|
+
* matching a key created as `my-key (old)`.
|
|
28
50
|
*/
|
|
29
51
|
const isListedAs = (listedName: string, name: string) => {
|
|
30
52
|
const prefix = `${name} (`;
|
|
31
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
listedName === name ||
|
|
55
|
+
(listedName.startsWith(prefix) && /^[^\s()]+\)$/.test(listedName.slice(prefix.length)))
|
|
56
|
+
);
|
|
32
57
|
};
|
|
33
58
|
|
|
34
59
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* another key with the same requested name, and its secret is lost.
|
|
60
|
+
* True when the listing still has the key in state: by numeric id, or by
|
|
61
|
+
* listed name for state written by 0.1.x, which has no id.
|
|
38
62
|
*/
|
|
39
|
-
export const
|
|
63
|
+
export const isKeyListed = (
|
|
64
|
+
entries: ReadonlyArray<typeof ListedKey.Type>,
|
|
65
|
+
key: { readonly keyId: number | undefined; readonly uniqueName: string },
|
|
66
|
+
): boolean =>
|
|
67
|
+
entries.some((entry) =>
|
|
68
|
+
key.keyId === undefined ? entry.name === key.uniqueName : entry.id === key.keyId,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/** The listed keys that Convex shows for a key created as `name`. */
|
|
72
|
+
export const keysListedAs = (
|
|
40
73
|
entries: ReadonlyArray<typeof ListedKey.Type>,
|
|
41
74
|
name: string,
|
|
42
|
-
): typeof ListedKey.Type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
): ReadonlyArray<typeof ListedKey.Type> => entries.filter((entry) => isListedAs(entry.name, name));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The name that a resource instance requests: `name`, a dash, and the first
|
|
79
|
+
* 12 hex characters of sha256("<fqn>:<instanceId>"). Two resources, or the
|
|
80
|
+
* old and the new generation of one replaced resource, never request the
|
|
81
|
+
* same name. Pattern from Confect `packages/alchemy/src/internal/ResourceIdentity.ts`
|
|
82
|
+
* (https://github.com/rjdellecese/confect, ISC license).
|
|
83
|
+
*/
|
|
84
|
+
export const requestedKeyName = (name: string, fqn: string, instanceId: string): string =>
|
|
85
|
+
`${name}-${createHash("sha256").update(`${fqn}:${instanceId}`).digest("hex").slice(0, 12)}`;
|
|
50
86
|
|
|
51
87
|
/** One row of list_deploy_keys and list_preview_deploy_keys. Only the fields the providers use. */
|
|
52
88
|
export const ListedKey = Schema.Struct({
|
|
89
|
+
id: Schema.Number,
|
|
53
90
|
name: Schema.String,
|
|
54
|
-
creationTime: Schema.Number,
|
|
55
91
|
});
|
|
56
92
|
|
|
57
93
|
/** GET /deployments/{deployment_name}/list_deploy_keys */
|
|
@@ -60,18 +96,132 @@ export const DeployKeyList = Schema.Array(ListedKey);
|
|
|
60
96
|
/** POST /deployments/{deployment_name}/create_deploy_key */
|
|
61
97
|
export const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
|
|
62
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A key cannot be tied to one resource: before the create, listed keys
|
|
101
|
+
* already have the requested name, or after the create, not exactly one has it.
|
|
102
|
+
*/
|
|
103
|
+
export class DeployKeyRecoveryRequired extends Schema.TaggedError<DeployKeyRecoveryRequired>()(
|
|
104
|
+
"DeployKeyRecoveryRequired",
|
|
105
|
+
{
|
|
106
|
+
/** Where the keys live, such as `deployment happy-otter-123` or `project 3145389`. */
|
|
107
|
+
target: Schema.String,
|
|
108
|
+
/** The requested name. */
|
|
109
|
+
name: Schema.String,
|
|
110
|
+
/** The listed keys with the requested name. */
|
|
111
|
+
keys: Schema.Array(ListedKey),
|
|
112
|
+
/** True when this run created a key before the listing check failed. */
|
|
113
|
+
afterCreate: Schema.Boolean,
|
|
114
|
+
},
|
|
115
|
+
) {
|
|
116
|
+
override get message() {
|
|
117
|
+
const listed =
|
|
118
|
+
this.keys.length === 0
|
|
119
|
+
? "no key"
|
|
120
|
+
: this.keys.map((key) => `"${key.name}" (id ${key.id})`).join(", ");
|
|
121
|
+
return this.afterCreate
|
|
122
|
+
? `Convex created deploy key "${this.name}" on ${this.target}, but the listing shows ${listed} with that name, not exactly one, so this resource cannot track the new key. Alchemy does not revoke it. Delete the keys with that name in the Convex dashboard, then run the deploy again.`
|
|
123
|
+
: `${this.target} already lists ${listed} with the name "${this.name}", but Alchemy state has no such key. An interrupted run probably created it, and its secret is lost. Delete these keys in the Convex dashboard, then run the deploy again.`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convex returned the Management API credential itself as the new key. This
|
|
129
|
+
* happens with an OAuth token: Convex creates no key. A delete of the
|
|
130
|
+
* returned secret would revoke the credential, so the resource does not keep it.
|
|
131
|
+
*/
|
|
132
|
+
export class DeployKeyIsCredential extends Schema.TaggedError<DeployKeyIsCredential>()(
|
|
133
|
+
"DeployKeyIsCredential",
|
|
134
|
+
{ target: Schema.String, name: Schema.String },
|
|
135
|
+
) {
|
|
136
|
+
override get message() {
|
|
137
|
+
return `Convex returned the Management API credential itself as deploy key "${this.name}" on ${this.target}. With an OAuth token, Convex creates no new key, and deleting the returned key would revoke the OAuth token. Alchemy does not keep it. Use a team access token in CONVEX_ACCESS_TOKEN, or the Convex CLI login.`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates a key named `name` and ties it to the caller. Fails before the
|
|
143
|
+
* create when keys with that name already exist. After the create, fails
|
|
144
|
+
* when the secret is the Management API credential itself, or when the
|
|
145
|
+
* listing does not show exactly one key with that name. It never revokes a
|
|
146
|
+
* key: the returned secret can be a shared credential.
|
|
147
|
+
*/
|
|
148
|
+
export const createTrackedKey = (options: {
|
|
149
|
+
readonly target: string;
|
|
150
|
+
readonly name: string;
|
|
151
|
+
readonly list: Effect.Effect<ReadonlyArray<typeof ListedKey.Type>, ConvexApiError>;
|
|
152
|
+
readonly create: Effect.Effect<string, ConvexApiError>;
|
|
153
|
+
readonly isCredentialToken: (secret: string) => Effect.Effect<boolean>;
|
|
154
|
+
}) =>
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
const { target, name } = options;
|
|
157
|
+
const existing = keysListedAs(yield* options.list, name);
|
|
158
|
+
if (existing.length > 0) {
|
|
159
|
+
return yield* new DeployKeyRecoveryRequired({
|
|
160
|
+
target,
|
|
161
|
+
name,
|
|
162
|
+
keys: existing,
|
|
163
|
+
afterCreate: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const secret = yield* options.create;
|
|
167
|
+
if (yield* options.isCredentialToken(secret)) {
|
|
168
|
+
return yield* new DeployKeyIsCredential({ target, name });
|
|
169
|
+
}
|
|
170
|
+
const listed = keysListedAs(yield* options.list, name);
|
|
171
|
+
const [only] = listed;
|
|
172
|
+
if (only === undefined || listed.length !== 1) {
|
|
173
|
+
return yield* new DeployKeyRecoveryRequired({
|
|
174
|
+
target,
|
|
175
|
+
name,
|
|
176
|
+
keys: listed,
|
|
177
|
+
afterCreate: true,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return { keyId: only.id, uniqueName: only.name, secret: Redacted.make(secret) };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The secret to send to a delete call, or undefined when a delete is not
|
|
185
|
+
* safe. Only a key with a `keyId` in state is known to be a new key that
|
|
186
|
+
* this resource owns: createTrackedKey found exactly one new listed key, and
|
|
187
|
+
* the secret was not the credential. State from 0.1.x has no `keyId`, and its
|
|
188
|
+
* secret can be an OAuth token that Convex returned as a key, so a delete
|
|
189
|
+
* could revoke that OAuth token. Such a key stays, and a warning names it.
|
|
190
|
+
*/
|
|
191
|
+
export const secretToDelete = (
|
|
192
|
+
key: {
|
|
193
|
+
readonly keyId: number | undefined;
|
|
194
|
+
readonly secret: Redacted.Redacted<string> | undefined;
|
|
195
|
+
},
|
|
196
|
+
label: string,
|
|
197
|
+
) =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
if (key.keyId === undefined || !Redacted.isRedacted(key.secret)) {
|
|
200
|
+
yield* Effect.logWarning(
|
|
201
|
+
`${label} has no verified key id and secret in state (state from 0.1.x, for example), so Alchemy does not delete it. Delete it in the Convex dashboard.`,
|
|
202
|
+
);
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return Redacted.value(key.secret);
|
|
206
|
+
});
|
|
207
|
+
|
|
63
208
|
export interface DeployKeyProps {
|
|
64
209
|
/** Deployment name, such as `project.prodDeploymentName`. */
|
|
65
210
|
readonly deployment: string;
|
|
66
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Key name. The provider requests this name, a dash, and a hash of the
|
|
213
|
+
* resource identity, so two resources with one `name` get different keys.
|
|
214
|
+
*/
|
|
67
215
|
readonly name: string;
|
|
68
216
|
/** Limits the key to these actions. Convex grants every deployment action when omitted. */
|
|
69
217
|
readonly allowedActions?: readonly DeployKeyAction[];
|
|
70
218
|
}
|
|
71
219
|
|
|
72
220
|
export interface DeployKeyAttributes {
|
|
73
|
-
/** The name Convex lists the key under, such as "
|
|
221
|
+
/** The name Convex lists the key under, such as "ci-3f2a1b0c9d8e". */
|
|
74
222
|
readonly uniqueName: string;
|
|
223
|
+
/** Numeric id of the key in the listing. Undefined in state written by 0.1.x. */
|
|
224
|
+
readonly keyId: number | undefined;
|
|
75
225
|
readonly deployment: string;
|
|
76
226
|
/** The secret, such as `prod:<deployment>|<token>`. Convex returns it only at creation. */
|
|
77
227
|
readonly deployKey: Redacted.Redacted<string>;
|
|
@@ -101,6 +251,20 @@ export const DeployKeyProvider = () =>
|
|
|
101
251
|
)
|
|
102
252
|
.pipe(Effect.map((body) => Schema.decodeUnknownSync(DeployKeyList)(body)));
|
|
103
253
|
|
|
254
|
+
/**
|
|
255
|
+
* `id` is the secret: Convex accepts it, and it matches only the key
|
|
256
|
+
* that this resource created. A second delete answers 404.
|
|
257
|
+
*/
|
|
258
|
+
const deleteKey = (deployment: string, secret: string) =>
|
|
259
|
+
retryIdempotentWrite(
|
|
260
|
+
api.requestVoid("delete deploy key", (client) =>
|
|
261
|
+
client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
262
|
+
params: { path: { deployment_name: deployment } },
|
|
263
|
+
body: { id: secret },
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
|
|
104
268
|
return {
|
|
105
269
|
stables: ["uniqueName", "deployment"],
|
|
106
270
|
|
|
@@ -119,52 +283,49 @@ export const DeployKeyProvider = () =>
|
|
|
119
283
|
// A key that is not in state has no known secret, so it is never adopted.
|
|
120
284
|
if (output === undefined) return undefined;
|
|
121
285
|
const entries = yield* absentAsUndefined(listKeys(output.deployment));
|
|
122
|
-
return entries
|
|
286
|
+
return entries !== undefined && isKeyListed(entries, output) ? output : undefined;
|
|
123
287
|
}),
|
|
124
288
|
|
|
125
|
-
reconcile: Effect.fn(function* ({ news, output }) {
|
|
289
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
126
290
|
if (output !== undefined) {
|
|
127
291
|
// The secret cannot be read back. A key that still exists keeps the one in state.
|
|
128
|
-
|
|
129
|
-
if (entries.some((entry) => entry.name === output.uniqueName)) return output;
|
|
292
|
+
if (isKeyListed(yield* listKeys(news.deployment), output)) return output;
|
|
130
293
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
294
|
+
const name = requestedKeyName(news.name, fqn, instanceId);
|
|
295
|
+
const { keyId, uniqueName, secret } = yield* createTrackedKey({
|
|
296
|
+
target: `deployment ${news.deployment}`,
|
|
297
|
+
name,
|
|
298
|
+
list: listKeys(news.deployment),
|
|
299
|
+
create: api
|
|
300
|
+
.request("create deploy key", (client) =>
|
|
301
|
+
client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
302
|
+
params: { path: { deployment_name: news.deployment } },
|
|
303
|
+
body: {
|
|
304
|
+
name,
|
|
305
|
+
...(news.allowedActions === undefined
|
|
306
|
+
? {}
|
|
307
|
+
: { allowedActions: [...news.allowedActions] }),
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
)
|
|
311
|
+
.pipe(
|
|
312
|
+
Effect.map((body) => Schema.decodeUnknownSync(CreatedDeployKey)(body).deployKey),
|
|
148
313
|
),
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
uniqueName: listed.name,
|
|
153
|
-
deployment: news.deployment,
|
|
154
|
-
deployKey: Redacted.make(deployKey),
|
|
155
|
-
};
|
|
314
|
+
isCredentialToken: api.isCredentialToken,
|
|
315
|
+
});
|
|
316
|
+
return { uniqueName, keyId, deployment: news.deployment, deployKey: secret };
|
|
156
317
|
}),
|
|
157
318
|
|
|
158
319
|
delete: Effect.fn(function* ({ output }) {
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
body: { id: output.uniqueName },
|
|
165
|
-
}),
|
|
166
|
-
),
|
|
320
|
+
// Only the secret names this key alone: 0.1.x state can hold the
|
|
321
|
+
// listed name of another resource's key.
|
|
322
|
+
const secret = yield* secretToDelete(
|
|
323
|
+
{ keyId: output.keyId, secret: output.deployKey },
|
|
324
|
+
`Convex.DeployKey "${output.uniqueName}" on ${output.deployment}`,
|
|
167
325
|
);
|
|
326
|
+
if (secret === undefined) return;
|
|
327
|
+
// 404 DeployKeyNotFound: an earlier attempt or someone else deleted it.
|
|
328
|
+
yield* absentAsUndefined(deleteKey(output.deployment, secret));
|
|
168
329
|
}),
|
|
169
330
|
};
|
|
170
331
|
}),
|