@samebase/alchemy-convex 0.23.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 +25 -17
- package/lib/{HubClient-B6hjkbjW.js → HubClient-Bs5_j18U.js} +45 -13
- package/lib/{HubClient-B6hjkbjW.js.map → HubClient-Bs5_j18U.js.map} +1 -1
- package/lib/cli.js +2 -2
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +33 -21
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HubClient.ts +70 -25
- package/src/SharedHubClient.ts +48 -30
- package/src/State.ts +1 -1
- package/src/cli.ts +7 -6
package/README.md
CHANGED
|
@@ -302,23 +302,29 @@ 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
|
-
|
|
306
|
-
credential and no hub address. The first client of an `app` creates
|
|
307
|
-
`clientId`, `clientSecret`, `redirectUri`, `deploymentType`, and
|
|
308
|
-
the `Convex.Deployment` writes to the deployment.
|
|
309
|
-
|
|
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
|
-
of the central identity hub
|
|
313
|
-
|
|
314
|
-
The origin of `redirectUri` must serve `/samebase-verification.json` with the
|
|
315
|
-
the repository, `{ "repository": "123" }`, which
|
|
316
|
-
|
|
317
|
-
`status`, `issuer`, `clientId`, `clientSecret`,
|
|
318
|
-
`registrationClientUri`, `deploymentType`, and `redirectUri`.
|
|
317
|
+
of the central identity hub, for an app that deploys in its own GitHub Actions run, on any host.
|
|
318
|
+
The workflow needs `permissions: id-token: write`, and no key and no Samebase account. `app` is at
|
|
319
|
+
most 64 characters. The origin of `redirectUri` must serve `/samebase-verification.json` with the
|
|
320
|
+
numeric GitHub id of the repository, `{ "repository": "123" }`, which
|
|
321
|
+
`gh api repos/<owner>/<name> --jq .id` prints. The hub reads it before it registers the client, so
|
|
322
|
+
the site must be live first. Outputs `status`, `issuer`, `clientId`, `clientSecret`,
|
|
323
|
+
`registrationAccessToken`, `registrationClientUri`, `deploymentType`, and `redirectUri`.
|
|
319
324
|
- Each deploy reads the saved client on the hub with its own token. A client that is gone gets a
|
|
320
325
|
new one, and a client on another `redirectUri` too.
|
|
321
|
-
- A
|
|
326
|
+
- A new client logs the link to the terms of the shared sign-in service. A refusal logs its
|
|
327
|
+
reason, such as a host that does not serve the file, a block, or the hourly limit it reached.
|
|
322
328
|
- No failure stops a deploy or a destroy. A failed renewal keeps the saved values. Without saved
|
|
323
329
|
values, `status` is `off`, and `issuer`, `clientId`, and `clientSecret` are the `no-hub` values
|
|
324
330
|
of a checkout without a hub. A run without a GitHub OIDC token, such as on a laptop, does the
|
|
@@ -326,12 +332,14 @@ call.
|
|
|
326
332
|
- `status` is `suspended` when the hub suspended the client. The client keeps its values.
|
|
327
333
|
- A destroy deletes a preview client. It does nothing to a production client, because the
|
|
328
334
|
production deployment can stay.
|
|
329
|
-
- `
|
|
335
|
+
- `HUB_URL` replaces `https://auth.samebase.com`, the hub that the run asks at `/hub-clients`.
|
|
330
336
|
|
|
331
337
|
A development deployment has no stack, so the package has a command for it. Run it in the directory
|
|
332
|
-
of the app, before the first push
|
|
333
|
-
|
|
334
|
-
|
|
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.
|
|
335
343
|
|
|
336
344
|
```sh
|
|
337
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
|
|
1874
|
-
* client on the hub, which
|
|
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
|
|
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
|
-
|
|
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.
|
|
1916
|
-
*
|
|
1917
|
-
*
|
|
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)))
|
|
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(
|
|
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-
|
|
1992
|
+
//# sourceMappingURL=HubClient-Bs5_j18U.js.map
|