@samebase/alchemy-convex 0.24.0 → 0.25.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 CHANGED
@@ -302,11 +302,16 @@ call.
302
302
  - `Convex.HubClient`: `{ team, app, redirectUri, deploymentType, clientName, softwareVersion? }`
303
303
  registers one deployment as a client of the identity hub of the team. The hub is the production
304
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.
305
+ functions `identityBroker:renewClient` and `identityBroker:registerClient` with a short-lived key,
306
+ so the stack needs no other credential and no hub address. The first client of an `app` creates
307
+ the app. Outputs `issuer`, `clientId`, `clientSecret`, `redirectUri`, `deploymentType`, and
308
+ `clientName`, which the `env` of the `Convex.Deployment` writes to the deployment.
309
+ - Each deploy renews the client on the hub. A preview or development client expires 14 days after
310
+ its last deploy or use, and a production client never expires. A client that the hub no longer
311
+ has gets a new one in the same run.
312
+ - Another `redirectUri` is a new client, and the old one goes.
313
+ - A destroy deletes a preview or development client. It does nothing to a production client,
314
+ because the production deployment can stay.
310
315
 
311
316
  - `Convex.SharedHubClient`: `{ app, redirectUri, deploymentType, softwareVersion? }` gets a client
312
317
  of the central identity hub, for an app that deploys in its own GitHub Actions run, on any host.
@@ -330,9 +335,11 @@ call.
330
335
  - `HUB_URL` replaces `https://auth.samebase.com`, the hub that the run asks at `/hub-clients`.
331
336
 
332
337
  A development deployment has no stack, so the package has a command for it. Run it in the directory
333
- of the app, before the first push: it registers the deployment of that directory with
334
- `CONVEX_ACCESS_TOKEN` or the Convex CLI login, and writes `HUB_ISSUER`, `HUB_CLIENT_ID`, and
335
- `HUB_CLIENT_SECRET` to it. A deployment with all three variables keeps them.
338
+ of the app, before the first push and at each dev start, with `CONVEX_ACCESS_TOKEN` or the Convex
339
+ CLI login. It renews the client that `HUB_CLIENT_ID` names, so the client expires 14 days after the
340
+ last dev start or use. A deployment without all three variables, or whose client the hub no longer
341
+ has, gets a new client, and the command writes `HUB_ISSUER`, `HUB_CLIENT_ID`, and
342
+ `HUB_CLIENT_SECRET` to it.
336
343
 
337
344
  ```sh
338
345
  npx alchemy-convex hub-client --team my-team --app scout --callback-path /auth/hub/callback
@@ -1846,6 +1846,8 @@ const Registered = Schema.Struct({
1846
1846
  clientId: Schema.NonEmptyString,
1847
1847
  clientSecret: Schema.NonEmptyString
1848
1848
  });
1849
+ /** The reply of `identityBroker:renewClient`: false when the hub no longer has the client. */
1850
+ const Renewed = Schema.Boolean;
1849
1851
  /** Runs `use` with the function API of the hub of `team`, with a key of this call only. */
1850
1852
  const withHub = (api, team, use) => Effect.gen(function* () {
1851
1853
  const teamId = yield* api.resolveTeamId(team);
@@ -1870,8 +1872,9 @@ const runHub = (functions, team, name, args) => functions.call(`identityBroker:$
1870
1872
  reason: reply.errorMessage.trim()
1871
1873
  }))));
1872
1874
  /**
1873
- * Registers one client. An attempt that a lost reply repeats leaves one spare
1874
- * client on the hub, which signs nobody in.
1875
+ * Registers one client, which expires unless it is a production client. An
1876
+ * attempt that a lost reply repeats leaves one spare client on the hub, which
1877
+ * signs nobody in, and which the hub deletes after 14 days.
1875
1878
  */
1876
1879
  const registerHubClient = (api, props) => withHub(api, props.team, (functions) => runHub(functions, props.team, "registerClient", {
1877
1880
  appId: props.app,
@@ -1879,7 +1882,8 @@ const registerHubClient = (api, props) => withHub(api, props.team, (functions) =
1879
1882
  deploymentType: props.deploymentType,
1880
1883
  clientName: props.clientName,
1881
1884
  softwareVersion: props.softwareVersion ?? "unknown",
1882
- tokenEndpointAuthMethod: "client_secret_post"
1885
+ tokenEndpointAuthMethod: "client_secret_post",
1886
+ expires: true
1883
1887
  })).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(Registered)(value).pipe(Effect.mapError(() => new HubClientFailed({
1884
1888
  team: props.team,
1885
1889
  reason: "registerClient gave no client"
@@ -1888,18 +1892,36 @@ const registerHubClient = (api, props) => withHub(api, props.team, (functions) =
1888
1892
  clientId,
1889
1893
  clientSecret: Redacted.make(clientSecret)
1890
1894
  })));
1895
+ /**
1896
+ * Renews one client: true while the hub has it. A preview or development client
1897
+ * then expires 14 days after this call.
1898
+ */
1899
+ const renewHubClient = (api, team, clientId) => withHub(api, team, (functions) => runHub(functions, team, "renewClient", {
1900
+ clientId,
1901
+ expires: true
1902
+ })).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(Renewed)(value).pipe(Effect.mapError(() => new HubClientFailed({
1903
+ team,
1904
+ reason: "renewClient gave no answer"
1905
+ })))));
1906
+ /**
1907
+ * Renews a client whose values the caller holds: false only when the hub answers that the client is
1908
+ * gone. Any other failure, such as a hub or a Convex API that cannot answer, keeps the client and
1909
+ * warns, so an outage never stops a deploy or a dev start; the next one renews it.
1910
+ */
1911
+ const renewOrKeep = (api, team, clientId) => renewHubClient(api, team, clientId).pipe(Effect.catch((error) => Effect.logWarning(`Convex.HubClient: ${clientId} keeps its saved values, because the hub could not renew it: ${error.message}`).pipe(Effect.as(true))));
1891
1912
  const unregister = (api, team, clientId) => withHub(api, team, (functions) => runHub(functions, team, "deleteClient", { clientId }));
1892
1913
  /** True when the client in state serves the redirect URI and deployment type of `props`. */
1893
1914
  const registeredFor = (output, props) => output.redirectUri === props.redirectUri && output.deploymentType === props.deploymentType;
1894
1915
  const HubClientProvider = () => Provider.effect(HubClient, Effect.gen(function* () {
1895
1916
  const api = yield* ManagementApi;
1896
1917
  return {
1897
- diff: ({ news, olds, output }) => Effect.succeed(!isResolved(news) || output === void 0 ? void 0 : !registeredFor(output, news) ? { action: "replace" } : olds.team === news.team && olds.app === news.app ? { action: "noop" } : { action: "update" }),
1918
+ diff: ({ news }) => Effect.succeed(isResolved(news) ? { action: "update" } : void 0),
1898
1919
  read: ({ output }) => Effect.succeed(output),
1899
1920
  reconcile: Effect.fn(function* ({ news, output }) {
1900
- if (output !== void 0 && registeredFor(output, news)) return output;
1921
+ const saved = output !== void 0 && registeredFor(output, news) ? output : void 0;
1922
+ if (saved !== void 0 && (yield* renewOrKeep(api, news.team, saved.clientId))) return saved;
1901
1923
  const client = yield* registerHubClient(api, news);
1902
- if (output !== void 0) yield* unregister(api, news.team, output.clientId).pipe(Effect.catchTag("HubClientFailed", (error) => Effect.logWarning(`Client ${output.clientId} stays on the hub: ${error.message} Delete it with \`npx convex run --prod identityBroker:deleteClient\`.`)));
1924
+ if (output !== void 0 && saved === void 0) yield* unregister(api, news.team, output.clientId).pipe(Effect.catchTag("HubClientFailed", (error) => Effect.logWarning(`Client ${output.clientId} stays on the hub: ${error.message} Delete it with \`npx convex run --prod identityBroker:deleteClient\`.`)));
1903
1925
  return {
1904
1926
  ...client,
1905
1927
  redirectUri: news.redirectUri,
@@ -1907,17 +1929,20 @@ const HubClientProvider = () => Provider.effect(HubClient, Effect.gen(function*
1907
1929
  clientName: news.clientName
1908
1930
  };
1909
1931
  }),
1910
- delete: ({ olds, output }) => unregister(api, olds.team, output.clientId).pipe(Effect.asVoid)
1932
+ delete: ({ olds, output }) => output.deploymentType === "production" ? Effect.void : unregister(api, olds.team, output.clientId).pipe(Effect.asVoid)
1911
1933
  };
1912
1934
  }));
1913
1935
  /**
1914
1936
  * `alchemy-convex hub-client`: a stack deploys production and previews, and this covers the
1915
- * development deployment, which no stack deploys. A deployment with all three variables keeps
1916
- * them. So a deployment that holds placeholder values for a checkout without a hub is left alone.
1917
- * The secret reaches the Convex CLI on standard input, never in its arguments.
1937
+ * development deployment, which no stack deploys. Run it at each dev start: it renews the client
1938
+ * that the three variables name, so the client expires 14 days after the last dev start or use. A
1939
+ * deployment without all three variables, or whose client the hub no longer has, such as one with
1940
+ * the `no-hub` values, gets a new client. Answers the callback URL of a new client, or undefined
1941
+ * for a renewal. The secret reaches the Convex CLI on standard input, never in its arguments.
1918
1942
  */
1919
1943
  const registerDevelopmentClient = (options) => Effect.gen(function* () {
1920
1944
  const { convex } = options;
1945
+ const api = yield* ManagementApi;
1921
1946
  const envPrefix = options.envPrefix ?? "HUB";
1922
1947
  const variables = [
1923
1948
  `${envPrefix}_ISSUER`,
@@ -1929,7 +1954,14 @@ const registerDevelopmentClient = (options) => Effect.gen(function* () {
1929
1954
  "list",
1930
1955
  "--names-only"
1931
1956
  ]).split("\n");
1932
- if (variables.every((name) => names.includes(name))) return void 0;
1957
+ if (variables.every((name) => names.includes(name))) {
1958
+ const clientId = convex([
1959
+ "env",
1960
+ "get",
1961
+ `${envPrefix}_CLIENT_ID`
1962
+ ]);
1963
+ if (yield* renewOrKeep(api, options.team, clientId)) return void 0;
1964
+ }
1933
1965
  const siteUrl = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(convex([
1934
1966
  "run",
1935
1967
  "--inline-query",
@@ -1939,7 +1971,7 @@ const registerDevelopmentClient = (options) => Effect.gen(function* () {
1939
1971
  reason: "no CONVEX_SITE_URL to register"
1940
1972
  })));
1941
1973
  const callbackUrl = `${siteUrl.replace(/\/$/, "")}${options.callbackPath}`;
1942
- const client = yield* registerHubClient(yield* ManagementApi, {
1974
+ const client = yield* registerHubClient(api, {
1943
1975
  team: options.team,
1944
1976
  app: options.app,
1945
1977
  redirectUri: callbackUrl,
@@ -1957,4 +1989,4 @@ const registerDevelopmentClient = (options) => Effect.gen(function* () {
1957
1989
  //#endregion
1958
1990
  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 };
1959
1991
 
1960
- //# sourceMappingURL=HubClient-B6hjkbjW.js.map
1992
+ //# sourceMappingURL=HubClient-Bs5_j18U.js.map