@samebase/alchemy-convex 0.18.0 → 0.19.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/README.md CHANGED
@@ -7,9 +7,11 @@ URLs to a Cloudflare Worker or a Vite site as ordinary outputs. No deploy key go
7
7
  to another. The Convex CLI that your app installs keeps doing typecheck, codegen, bundling, and the
8
8
  push.
9
9
 
10
- Alchemy has no built-in Convex provider ([alchemy-run/alchemy-async#1306](https://github.com/alchemy-run/alchemy-async/issues/1306)).
11
- This is a community provider, maintained by [Samebase](https://samebase.com). The resource model
12
- follows the "Convex Plain" mode of [rjdellecese/confect#408](https://github.com/rjdellecese/confect/issues/408).
10
+ Alchemy has no built-in Convex provider
11
+ ([alchemy-run/alchemy-async#1306](https://github.com/alchemy-run/alchemy-async/issues/1306)). This
12
+ is a community provider, maintained by [Samebase](https://samebase.com). The resource model follows
13
+ the "Convex Plain" mode of
14
+ [rjdellecese/confect#408](https://github.com/rjdellecese/confect/issues/408).
13
15
  [docs/design/convex-plane.md](docs/design/convex-plane.md) records the decisions.
14
16
 
15
17
  Status: pinned to `alchemy@2.0.0-beta.84` and Effect 4. Alchemy ships breaking changes between
@@ -66,16 +68,15 @@ export default Alchemy.Stack(
66
68
  The imports are `alchemy`, `@alchemy.run/cloudflare`, `alchemy/RemovalPolicy`, `effect/Config`,
67
69
  `effect/Effect`, `effect/Layer`, and `@samebase/alchemy-convex` as `Convex`. Order follows the
68
70
  outputs: the project exists before its deployment, and the push finishes before the site build. The
69
- first `alchemy deploy --stage prod` takes over the existing project, and its production
70
- deployment if it has code, only with `--adopt`.
71
- `alchemy destroy --stage pr-42` deletes the preview deployment. Without `RemovalPolicy.destroy()`,
72
- it stays until Convex deletes it at expiry (14 days by default).
71
+ first `alchemy deploy --stage prod` takes over the existing project, and its production deployment
72
+ if it has code, only with `--adopt`. `alchemy destroy --stage pr-42` deletes the preview deployment.
73
+ Without `RemovalPolicy.destroy()`, it stays until Convex deletes it at expiry (14 days by default).
73
74
  [`examples/app/alchemy.run.ts`](examples/app/alchemy.run.ts) is the same stack.
74
75
 
75
76
  ### The stack file at the repository root
76
77
 
77
- `rootDir` is the app directory, relative to the directory where `alchemy deploy` runs, which is
78
- the directory of the stack file. The default is `"."`. The push runs in `rootDir`, and `source` is
78
+ `rootDir` is the app directory, relative to the directory where `alchemy deploy` runs, which is the
79
+ directory of the stack file. The default is `"."`. The push runs in `rootDir`, and `source` is
79
80
  relative to it:
80
81
 
81
82
  ```ts
@@ -128,10 +129,9 @@ The deployment's Alchemy state row stores this entry in its native `bindings` ar
128
129
  }
129
130
  ```
130
131
 
131
- Readers inspect each binding's `data`. The deployment attributes identify the Convex deployment;
132
- the row's stack and stage identify the Alchemy scope. The live repository reader includes the
133
- binding through its existing resource JSON. No extra resource list, state table, or subscription
134
- is needed.
132
+ Readers inspect each binding's `data`. The deployment attributes identify the Convex deployment; the
133
+ row's stack and stage identify the Alchemy scope. The live repository reader includes the binding
134
+ through its existing resource JSON. No extra resource list, state table, or subscription is needed.
135
135
 
136
136
  Use actual namespace ancestry when grouping resources. A slash in a resource's logical ID is not a
137
137
  namespace boundary. For a nested install, join the installed names with slashes, such as
@@ -158,8 +158,8 @@ reads them through the stack's config provider (process env, `.env`, stack secre
158
158
  actions), because the provider reads the project and type of its deployment first.
159
159
  - A preview deploy key of a project (`preview:<team>:<project>|...`) creates, pushes to, and
160
160
  deletes preview deployments of that project. It cannot change variables: with `env`, the
161
- provider uses the access token, and without one it stops with `EnvironmentNeedsDeployKey`
162
- before it creates anything. Use project default variables in the Convex dashboard for previews.
161
+ provider uses the access token, and without one it stops with `EnvironmentNeedsDeployKey` before
162
+ it creates anything. Use project default variables in the Convex dashboard for previews.
163
163
 
164
164
  A key that does not fit the deployment is not used: the provider uses the access token. To pass
165
165
  values explicitly: `Convex.providers(Convex.fromValues({ accessToken, deployKey }))`.
@@ -168,9 +168,9 @@ values explicitly: `Convex.providers(Convex.fromValues({ accessToken, deployKey
168
168
 
169
169
  `state: Convex.state({ team })` keeps the Alchemy state of every stack and stage of a Convex team in
170
170
  one project of that team, `alchemy-state-store`, the same name as the Cloudflare state store of
171
- Alchemy, so a repository holds one secret and a stack can create
172
- and destroy its own project. [docs/design/convex-state.md](docs/design/convex-state.md) records the
173
- decisions and the measurements.
171
+ Alchemy, so a repository holds one secret and a stack can create and destroy its own project.
172
+ [docs/design/convex-state.md](docs/design/convex-state.md) records the decisions and the
173
+ measurements.
174
174
 
175
175
  ```ts
176
176
  export default Alchemy.Stack(
@@ -182,27 +182,52 @@ export default Alchemy.Stack(
182
182
  );
183
183
  ```
184
184
 
185
- Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login on a laptop. A
186
- team access token names its team, so `Convex.state()` needs no `team` with one; a `team` slug that
187
- is set must be the slug of that team, checked against its projects, and a token of another team
188
- fails the run. The Convex CLI login needs the slug or the id. The first run of a team creates
189
- the project `alchemy-state-store` and pushes the store functions to its
190
- production deployment with the Convex CLI of the app; a later version of this package pushes a
191
- newer store when it finds an older one. A store that is newer than the package fails the run with
192
- both versions, so the repositories of a team upgrade the package together. Each run makes a
193
- deploy key of that deployment that can only run the store's internal functions, and deletes it at
194
- the end. Rows are in the tables `resources` and `outputs` of the component `alchemyState` in the
195
- dashboard, under the stack name.
196
- The store has no lock: run one deploy of a stage at a time, such as with a CI concurrency group.
197
- The store and its readers (Samebase) are released and upgraded together.
198
-
199
- Store version 3 supports native subscriptions. A trusted server checks repository access, then
200
- calls internal `reader:authorize({ repository, subject })` with a deployment credential. It gives
201
- the returned token to a separate `ConvexClient` connected to the state deployment. The client
202
- subscribes to `reader:repository({})`; the verified token selects the repository. The token has a
203
- five-minute lifetime and permits redacted reads only. The server checks access again on renewal.
204
- The store uses its own native signing key and JWKS, so no extra signing secret is required.
205
- See [the reader contract](docs/design/convex-state.md#repository-subscriptions-090).
185
+ Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login on a laptop. A team
186
+ access token names its team, so `Convex.state()` needs no `team` with one; a `team` slug that is set
187
+ must be the slug of that team, checked against its projects, and a token of another team fails the
188
+ run. The Convex CLI login needs the slug or the id. The first run of a team creates the project
189
+ `alchemy-state-store` and pushes the store functions to its production deployment with the Convex
190
+ CLI of the app; a later version of this package pushes a newer store when it finds an older one. A
191
+ store that is newer than the package fails the run with both versions, so the repositories of a team
192
+ upgrade the package together. Each run makes a deploy key of that deployment that can only run the
193
+ store's internal functions, and deletes it at the end. Rows are in the tables `resources` and
194
+ `outputs` of the component `alchemyState` in the dashboard, under the stack name. The store has no
195
+ lock: run one deploy of a stage at a time, such as with a CI concurrency group. The store and its
196
+ readers (Samebase) are released and upgraded together.
197
+
198
+ Store version 3 supports native subscriptions. A trusted server checks repository access, then calls
199
+ internal `reader:authorize({ repository, subject })` with a deployment credential. It gives the
200
+ returned token to a separate `ConvexClient` connected to the state deployment. The client subscribes
201
+ to `reader:repository({})`; the verified token selects the repository. The token has a five-minute
202
+ lifetime and permits redacted reads only. The server checks access again on renewal. The store uses
203
+ its own native signing key and JWKS, so no extra signing secret is required. See
204
+ [the reader contract](docs/design/convex-state.md#repository-subscriptions-090).
205
+
206
+ ### Secrets in the rows
207
+
208
+ Alchemy writes a `Redacted` value in clear text, and a row must keep it: the hub keeps only the hash
209
+ of a client secret, so the next run can get the value only from the state. The store seals each of
210
+ these values with AES-256-GCM before it writes the row. A table view, a snapshot, and a reader that
211
+ forgets to remove secrets show no value.
212
+
213
+ Each key is one variable `STATE_KEY_<keyId>` of the store deployment, and a sealed value names its
214
+ key. A run that finds no key makes one, writes it to the deployment, and never reads it back. A
215
+ transaction in the store chooses the current key, so two first runs cannot replace a key that a row
216
+ needs. Each run asks if every row is sealed under the current key, and finishes the work of a run
217
+ that stopped.
218
+
219
+ The functions that a run reads with open the values, so a holder of the team token or of a key that
220
+ can run internal queries still reads every secret of the team. A value that is not `Redacted`, such
221
+ as a plain string in `env`, stays clear text.
222
+
223
+ To rotate the key, from a directory that selects the project `alchemy-state-store`:
224
+
225
+ 1. Add a key with a new id of letters and digits:
226
+ `npx convex env set --prod STATE_KEY_<keyId> "$(openssl rand -base64 32)"`.
227
+ 2. Make it the current key: `npx convex run --prod state:rotateKey '{"keyId":"<keyId>"}'`.
228
+ 3. The next run seals every value under it. To do this now: `npx convex run --prod state:reseal`.
229
+ 4. When `npx convex run --prod state:sealing` answers `"sealed": true`, remove the old variable. A
230
+ key that left the deployment before that cannot open its values.
206
231
 
207
232
  ### What the output records about a run
208
233
 
@@ -241,15 +266,15 @@ call.
241
266
 
242
267
  - `Convex.Project`: `{ team, name, deploymentRegion?, deploymentType? }` creates a project with its
243
268
  default deployment, or takes over one with that exact name with `--adopt`. A changed `name`
244
- renames the project in place. `{ team, slug }` is an existing project, found by its slug and
245
- taken over with `--adopt`; it never creates or renames a project. Outputs `projectId`, `slug`,
246
- `name`, `teamId`, `prodDeploymentName`, `prodDeploymentUrl`.
269
+ renames the project in place. `{ team, slug }` is an existing project, found by its slug and taken
270
+ over with `--adopt`; it never creates or renames a project. Outputs `projectId`, `slug`, `name`,
271
+ `teamId`, `prodDeploymentName`, `prodDeploymentUrl`.
247
272
  - `Convex.Deployment`: `{ project, type: "prod" }` or `{ project, type: "dev" | "preview", name }`,
248
273
  each with optional `rootDir`, `source`, and `env`. Outputs `name` (such as `happy-animal-123`),
249
274
  `url`, `siteUrl`, `type`, `projectId`, `reference`, `previewName`, `hash`, and `envNames`.
250
- - `prod` is the project's default production deployment (with a deploy key: the key's
251
- production deployment). The resource never creates or deletes one. A production deployment
252
- with code that is not in state is taken over only with `--adopt`.
275
+ - `prod` is the project's default production deployment (with a deploy key: the key's production
276
+ deployment). The resource never creates or deletes one. A production deployment with code that
277
+ is not in state is taken over only with `--adopt`.
253
278
  - `dev` creates a dev deployment with `name` as its reference; `preview` creates a preview
254
279
  deployment with `name` as its preview name. An expired preview is created again on the next
255
280
  deploy.
@@ -264,24 +289,24 @@ call.
264
289
  lockfile, the deployment name, and `env`. The same hash is a no-op. Otherwise the update sets
265
290
  `env`, then pushes. A failed push sets the changed variables back, and deletes a dev or preview
266
291
  deployment that the same run created.
267
- - `env` values are strings or secrets such as `Config.Redacted("AUTH_SECRET")`. A name that
268
- leaves `env` is removed from the deployment.
292
+ - `env` values are strings or secrets such as `Config.Redacted("AUTH_SECRET")`. A name that leaves
293
+ `env` is removed from the deployment.
269
294
  - `source: { path, command?, include? }` replaces `convex deploy` with `command`, run in `rootDir`
270
295
  with the push key in `CONVEX_DEPLOY_KEY`, such as
271
296
  `["node_modules/.bin/convex-hosting", "deploy", "--build", "node_modules/.bin/vp build", "--dist", "dist/client"]`.
272
297
  The command must push with `convex deploy` (convex-hosting does, through `--cmd`). `include`
273
298
  narrows the hash to `path`, package.json, convex.json, the lockfile, and the listed globs, such
274
- as the build inputs `["src/**", "public/**", "vite.config.ts"]`. A minted key for a command
275
- has every action, because the command can do more than a push.
299
+ as the build inputs `["src/**", "public/**", "vite.config.ts"]`. A minted key for a command has
300
+ every action, because the command can do more than a push.
276
301
 
277
302
  - `Convex.HubClient`: `{ team, app, redirectUri, deploymentType, clientName, softwareVersion? }`
278
303
  registers one deployment as a client of the identity hub of the team. The hub is the production
279
- deployment of the project `identity-hub`. The resource finds it by name and runs
280
- its internal function `identityBroker:registerClient` with a short-lived key, so the stack needs no
281
- other credential and no hub address. The first client of an `app` creates the app. Outputs
282
- `issuer`, `clientId`, `clientSecret`, `redirectUri`, `deploymentType`, and `clientName`, which the
283
- `env` of the `Convex.Deployment` writes to the deployment. Another `redirectUri` is a new client, and
284
- a destroy deletes the client.
304
+ deployment of the project `identity-hub`. The resource finds it by name and runs its internal
305
+ function `identityBroker:registerClient` with a short-lived key, so the stack needs no other
306
+ credential and no hub address. The first client of an `app` creates the app. Outputs `issuer`,
307
+ `clientId`, `clientSecret`, `redirectUri`, `deploymentType`, and `clientName`, which the `env` of
308
+ the `Convex.Deployment` writes to the deployment. Another `redirectUri` is a new client, and a
309
+ destroy deletes the client.
285
310
 
286
311
  A development deployment has no stack, so the package has a command for it. Run it in the directory
287
312
  of the app, before the first push: it registers the deployment of that directory with
@@ -329,8 +354,8 @@ repository `owner/name`. A stack without the stage of the pull request has no ca
329
354
  `DeploymentIdentityChange`. A production deployment is never deleted, also not with
330
355
  `RemovalPolicy.destroy()`, and also not a dev deployment that became one.
331
356
  - A dev or preview deployment that exists but is not in state belongs to someone else, such as a
332
- preview that CI made. The resource takes it over only with `--adopt`, and never creates over it:
333
- a Convex create for an existing preview name deletes that preview.
357
+ preview that CI made. The resource takes it over only with `--adopt`, and never creates over it: a
358
+ Convex create for an existing preview name deletes that preview.
334
359
  - With the access token, each push uses a new deploy key with only `deployment:deploy`,
335
360
  `deployment:env:view`, and `deployment:env:write`, which expires after two hours and is deleted
336
361
  after the push, also after a failure. With an OAuth token, Convex returns the token itself as the
@@ -347,8 +372,9 @@ repository `owner/name`. A stack without the stage of the pull request has no ca
347
372
 
348
373
  ## Limits
349
374
 
350
- - `env` values are props, so Alchemy state holds them. Keep `.alchemy/` git-ignored, and use an
351
- encrypted state store, such as `Cloudflare.state()`, in CI.
375
+ - `env` values are props, so Alchemy state holds them. `Convex.state()` seals the `Redacted` values
376
+ (see [Secrets in the rows](#secrets-in-the-rows)). Use `Config.Redacted` for a secret, and keep
377
+ `.alchemy/` git-ignored.
352
378
  - The hash does not see a change in a workspace package outside `rootDir`, or a push from outside
353
379
  Alchemy. Change a file of the app to push again.
354
380
  - With a deploy key, the provider reaches the deployment at `https://<name>.convex.cloud`, so the
@@ -362,21 +388,22 @@ repository `owner/name`. A stack without the stage of the pull request has no ca
362
388
 
363
389
  ## Development
364
390
 
391
+ This package is in the [Samebase repository](https://github.com/samebase/samebase), in
392
+ `packages/alchemy-convex`. Run these commands from this folder:
393
+
365
394
  ```sh
366
395
  pnpm install
367
- pnpm run check # format, lint, typecheck, unit tests
368
- pnpm run build # lib/ with declarations, then publint and attw
396
+ pnpm run check # typecheck, lint, unit tests, build
369
397
  pnpm run test:live # real API calls, see below
370
398
  ```
371
399
 
372
- Use Node 24 (`.node-version`) and pnpm 11. Unit tests run against recorded Management API payloads
373
- in `test/fixtures/management/` and a fake Convex CLI. Live tests run only with
374
- `ALCHEMY_CONVEX_LIVE=1`, which `pnpm run test:live` sets. They need a Convex login or
375
- `CONVEX_ACCESS_TOKEN` that can create projects in `ALCHEMY_CONVEX_LIVE_TEAM`. Each test creates a
376
- project whose name starts with `ALCHEMY_CONVEX_LIVE_PREFIX` (default `tmp-alchemy-convex-`),
377
- deletes it at the end, also after a failure, and refuses to delete a project, or a deployment of a
378
- project, without that prefix. A file argument runs one live file only, for example
379
- `pnpm run test:live test/live/plane.live.test.ts`.
400
+ Unit tests run against recorded Management API payloads in `test/fixtures/management/` and a fake
401
+ Convex CLI. Live tests run only with `ALCHEMY_CONVEX_LIVE=1`, which `pnpm run test:live` sets. They
402
+ need a Convex login or `CONVEX_ACCESS_TOKEN` that can create projects in `ALCHEMY_CONVEX_LIVE_TEAM`.
403
+ Each test creates a project whose name starts with `ALCHEMY_CONVEX_LIVE_PREFIX` (default
404
+ `tmp-alchemy-convex-`), deletes it at the end, also after a failure, and refuses to delete a
405
+ project, or a deployment of a project, without that prefix. A file argument runs one live file only,
406
+ for example `pnpm run test:live test/live/plane.live.test.ts`.
380
407
 
381
408
  Releases: change `version` in `package.json` in a pull request. After the merge,
382
409
  `.github/workflows/release.yml` publishes that version from `main` with npm trusted publishing.
@@ -1,3 +1,4 @@
1
+ import { n as newKeyId, t as newKey } from "./sealedSecret-Bf8AeIp_.js";
1
2
  import { createRequire } from "node:module";
2
3
  import { Stack } from "alchemy/Stack";
3
4
  import * as Effect from "effect/Effect";
@@ -181,7 +182,11 @@ const TokenDetails = Schema.Union([Schema.Struct({
181
182
  projectId: Schema.Number
182
183
  })]);
183
184
  /** The service for one way to get the bearer token. */
184
- const makeService = (credentials) => {
185
+ const makeService = (source) => {
186
+ const credentials = source.pipe(Effect.map((resolved) => ({
187
+ ...resolved,
188
+ accessToken: Redacted.make(Redacted.value(resolved.accessToken).trim())
189
+ })));
185
190
  const client = Effect.cached(credentials.pipe(Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))))).pipe(Effect.flatten);
186
191
  const request = (operation, run) => client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
187
192
  const requestVoid = (operation, run) => client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
@@ -239,6 +244,7 @@ const makeService = (credentials) => {
239
244
  return match.id;
240
245
  });
241
246
  const isCredentialToken = (secret) => credentials.pipe(Effect.map((resolved) => tokenPart(Redacted.value(resolved.accessToken)) === tokenPart(secret)));
247
+ const isOAuthCredential = credentials.pipe(Effect.map((resolved) => /^(team|project):/.test(Redacted.value(resolved.accessToken))));
242
248
  const withToken = (token) => makeService(Effect.succeed({
243
249
  accessToken: token,
244
250
  source: "explicit"
@@ -249,6 +255,7 @@ const makeService = (credentials) => {
249
255
  deploymentRequest,
250
256
  resolveTeamId,
251
257
  isCredentialToken,
258
+ isOAuthCredential,
252
259
  withToken
253
260
  };
254
261
  };
@@ -475,13 +482,15 @@ const cliEnvironment = (home) => ({
475
482
  /**
476
483
  * A deploy key of `deployment` that expires after PUSH_KEY_LIFETIME_MS, with
477
484
  * only `allowedActions`, or every action when that is undefined. Delete it
478
- * with {@link deleteDeployKey} when the work is done.
485
+ * with {@link deleteDeployKey} when the work is done. An OAuth credential sends
486
+ * only the name: Convex refuses both limits and returns the OAuth token.
479
487
  */
480
488
  const createDeployKey = (api, deployment, name, allowedActions) => Effect.gen(function* () {
481
489
  const now = yield* Clock.currentTimeMillis;
490
+ const oauth = yield* api.isOAuthCredential;
482
491
  const created = yield* api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
483
492
  params: { path: { deployment_name: deployment } },
484
- body: {
493
+ body: oauth ? { name } : {
485
494
  name,
486
495
  ...allowedActions === void 0 ? {} : { allowedActions: [...allowedActions] },
487
496
  expiresAt: now + PUSH_KEY_LIFETIME_MS
@@ -1207,7 +1216,7 @@ const listProjectsByName = (api, teamId, name) => Effect.gen(function* () {
1207
1216
  const matches = [];
1208
1217
  const seenCursors = /* @__PURE__ */ new Set();
1209
1218
  let cursor;
1210
- while (true) {
1219
+ for (;;) {
1211
1220
  const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
1212
1221
  path: { team_id: teamId },
1213
1222
  query: {
@@ -1508,6 +1517,10 @@ const withRunRecord = (output, record) => isPlainObject(output) ? "samebase.run"
1508
1517
  [RUN_RECORD_KEY]: record
1509
1518
  } : void 0;
1510
1519
  //#endregion
1520
+ //#region src/store/convex/sealing.ts
1521
+ /** The variable of the store deployment that holds the key `keyId`. */
1522
+ const stateKeyName = (keyId) => `STATE_KEY_${keyId}`;
1523
+ //#endregion
1511
1524
  //#region src/State.ts
1512
1525
  /** The name of the state project in each team. */
1513
1526
  const STORE_PROJECT_NAME = "alchemy-state-store";
@@ -1517,6 +1530,8 @@ const STORE_PROJECT_NAME = "alchemy-state-store";
1517
1530
  * `deployment:data:write` do not run an internal function.
1518
1531
  */
1519
1532
  const RUN_KEY_ACTIONS = ["deployment:functions:runInternalQueries", "deployment:functions:runInternalMutations"];
1533
+ /** The actions of the key of one run on the store: its writes are actions that seal. */
1534
+ const STORE_KEY_ACTIONS = [...RUN_KEY_ACTIONS, "deployment:functions:runInternalActions"];
1520
1535
  /** The body of POST /api/function, with HTTP 200 for a function error too. */
1521
1536
  const Reply = Schema.Union([Schema.Struct({
1522
1537
  status: Schema.Literal("success"),
@@ -1557,6 +1572,13 @@ const post = (url, headers, body) => Effect.tryPromise({
1557
1572
  times: 5
1558
1573
  }));
1559
1574
  const decode = (schema, what) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
1575
+ /** The reply of `state:sealing`. */
1576
+ const Sealing = Schema.Struct({
1577
+ current: Schema.NullOr(Schema.String),
1578
+ sealed: Schema.Boolean
1579
+ });
1580
+ /** The reply of `state:reseal`. */
1581
+ const Resealed = Schema.Struct({ failed: Schema.Array(Schema.String) });
1560
1582
  const makeFunctionApi = (url, key) => ({ call: (path, args) => post(`${url}/api/function`, { Authorization: `Convex ${Redacted.value(key)}` }, {
1561
1583
  path,
1562
1584
  args,
@@ -1639,7 +1661,7 @@ const storeDirectory = () => join(dirname(fileURLToPath(import.meta.url)), "..",
1639
1661
  */
1640
1662
  const pushStore = (options, deployment) => Effect.gen(function* () {
1641
1663
  const cli = yield* installedCli(join(options.store, ".."));
1642
- yield* Effect.logInfo(`Convex state: pushing the store version 4 to ${deployment.name}`);
1664
+ yield* Effect.logInfo(`Convex state: pushing the store version 5 to ${deployment.name}`);
1643
1665
  yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
1644
1666
  deployment: deployment.name,
1645
1667
  url: deployment.deploymentUrl,
@@ -1662,6 +1684,26 @@ const pushStore = (options, deployment) => Effect.gen(function* () {
1662
1684
  }));
1663
1685
  });
1664
1686
  /**
1687
+ * Finishes the sealing of the store, whatever an earlier run left: a new key when the store has
1688
+ * none, then a reseal unless one finished under the current key. A run reads this each time, so a
1689
+ * run that stopped after a push cannot leave rows in clear text. This client makes a key once, and
1690
+ * it never reads a key back.
1691
+ */
1692
+ const ensureSealed = (options, deployment, functions) => Effect.gen(function* () {
1693
+ const { current, sealed } = yield* functions.call("state:sealing", {}).pipe(Effect.flatMap(valueOf), Effect.flatMap(decode(Sealing, "state:sealing")));
1694
+ if (current === null) {
1695
+ const keyId = newKeyId();
1696
+ yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => writeVariables(deployment.deploymentUrl, key, [{
1697
+ name: stateKeyName(keyId),
1698
+ value: newKey()
1699
+ }]));
1700
+ yield* functions.call("state:chooseKey", { keyId }).pipe(Effect.flatMap(valueOf));
1701
+ }
1702
+ if (sealed) return;
1703
+ const { failed } = yield* functions.call("state:reseal", {}).pipe(Effect.flatMap(valueOf), Effect.flatMap(decode(Resealed, "state:reseal")));
1704
+ if (failed.length > 0) yield* Effect.logWarning(`Convex state: the store could not seal ${failed.join(", ")}. The next run tries again.`);
1705
+ });
1706
+ /**
1665
1707
  * Creates the state project of the team. Two first runs can both create one:
1666
1708
  * the run whose project is not the oldest deletes its own, which holds
1667
1709
  * nothing yet, and uses the oldest, so that every run ends on one store.
@@ -1693,7 +1735,7 @@ const makeConvexState = (options) => Effect.gen(function* () {
1693
1735
  if (deployment === void 0) return yield* new ProductionDeploymentNotFound({ projectId: project.id });
1694
1736
  const keys = [];
1695
1737
  yield* Scope.addFinalizer(options.scope, Effect.suspend(() => Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), { discard: true })));
1696
- const mint = createDeployKey(api, deployment.name, STORE_PROJECT_NAME, RUN_KEY_ACTIONS).pipe(Effect.map((key) => {
1738
+ const mint = createDeployKey(api, deployment.name, STORE_PROJECT_NAME, STORE_KEY_ACTIONS).pipe(Effect.map((key) => {
1697
1739
  keys.push(key);
1698
1740
  return makeFunctionApi(deployment.deploymentUrl, key);
1699
1741
  }), Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)));
@@ -1703,15 +1745,16 @@ const makeConvexState = (options) => Effect.gen(function* () {
1703
1745
  return next.call(path, args);
1704
1746
  })))) };
1705
1747
  let version = yield* storeVersion(functions);
1706
- if (version === void 0 || version < 4) {
1748
+ if (version === void 0 || version < 5) {
1707
1749
  if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`);
1708
1750
  version = yield* storeVersion(functions);
1709
- if (version === void 0 || version < 4) {
1751
+ if (version === void 0 || version < 5) {
1710
1752
  yield* pushStore(options, deployment);
1711
1753
  version = yield* storeVersion(functions);
1712
1754
  }
1713
1755
  }
1714
- if (version !== 4) return yield* failure(`${deployment.name} answers store version ${version ?? "none"}, and this package needs store version 4.`);
1756
+ if (version !== 5) return yield* failure(`${deployment.name} answers store version ${version ?? "none"}, and this package needs store version 5.`);
1757
+ yield* ensureSealed(options, deployment, functions);
1715
1758
  const remote = remoteOf(functions, options.run);
1716
1759
  /** The stack's output plus the record, when the run has one and the output has a place for it. */
1717
1760
  const documentOf = (request) => Effect.gen(function* () {
@@ -1910,4 +1953,4 @@ const registerDevelopmentClient = (options) => Effect.gen(function* () {
1910
1953
  //#endregion
1911
1954
  export { PushTargetMismatch as A, DEPLOY_KEY_ENV as B, DeploymentProvider as C, ConvexAppNotFound as D, CodePushFailed as E, ManagementApi as F, fromValues as H, ManagementApiLive as I, ACCESS_TOKEN_ENV as L, installedCli as M, parseDeployOutput as N, ConvexCliNotFound as O, ConvexApiError as P, Credentials as R, DeploymentIdentityChange as S, ProductionDeploymentNotFound as T, fromConfig as V, ProjectProvider as _, registerDevelopmentClient as a, AmbiguousDeployment as b, RUN_RECORD_KEY as c, withRunRecord as d, AmbiguousProject as f, ProjectNotFound as g, ProjectDeploymentChange as h, HubClientProvider as i, VariablesOwnedBySomeoneElse as j, EnvironmentNeedsDeployKey as k, RunRecordError as l, Project as m, HubClient as n, STORE_PROJECT_NAME as o, CreatedProjectNotFound as p, HubClientFailed as r, state as s, HUB_PROJECT_NAME as t, runRecord as u, ProjectTeamChange as v, DeploymentResource as w, Deployment as x, AbsoluteRootDir as y, CredentialsError as z };
1912
1955
 
1913
- //# sourceMappingURL=HubClient-BZKweNfD.js.map
1956
+ //# sourceMappingURL=HubClient-NUvPkolM.js.map