@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/lib/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { I as ManagementApiLive, M as installedCli, V as fromConfig, a as registerDevelopmentClient, c as RUN_RECORD_KEY, s as state, u as runRecord } from "./HubClient-B6hjkbjW.js";
2
+ import { I as ManagementApiLive, M as installedCli, V as fromConfig, a as registerDevelopmentClient, c as RUN_RECORD_KEY, s as state, u as runRecord } from "./HubClient-Bs5_j18U.js";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Schema from "effect/Schema";
5
5
  import * as Layer from "effect/Layer";
@@ -242,7 +242,7 @@ const hubClient = (hub) => Effect.gen(function* () {
242
242
  ]
243
243
  }).trim()
244
244
  });
245
- console.log(callbackUrl === void 0 ? "Hub client already registered." : `Hub client registered for ${callbackUrl}.`);
245
+ console.log(callbackUrl === void 0 ? "Hub client renewed." : `Hub client registered for ${callbackUrl}.`);
246
246
  }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
247
247
  const stackComment = (options) => updateStackComment.pipe(Effect.tap((url) => Effect.sync(() => console.log(url))), Effect.provide(state(options).pipe(Layer.provide(NodeServices.layer))));
248
248
  const subcommand = positionals.join(" ");
package/lib/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/StackComment.ts","../src/cli.ts"],"sourcesContent":["// The command `alchemy-convex stack-comment`: one comment on a pull request with the stacks of the\n// repository, read from the state store after the deploys.\n//\n// A stack sees only itself, so a comment that a stack declares cannot show the other stacks of the\n// repository. This command runs as a step after them and reads each stack named `owner/name` or\n// `owner/name/<folder>` that has the stage `pr-<number>`. A stack without that stage has no card. A\n// run replaces the whole body, so the last run shows the newest state.\n//\n// It reads only the fields of the rows below, because Alchemy state holds secrets in clear text.\n//\n// GitHub keeps a fixed set of HTML in a comment and removes `style` and `class`, so every choice of\n// the markup is a fact about the sanitizer or the CSS of github.com:\n// - A table has `display: block` and `width: max-content`, so a `width` on it does nothing. The\n// spacer, a floated image one pixel high in a block of its own, gives every card one width, and\n// it shrinks on a narrow page.\n// - The CSS colors the even rows of a group, so each row has its own `tbody`.\n// - An HTML block ends at a blank line, so a table has none and is not indented.\n// - A bare image links to its raw file, and one link around an image and text underlines the space\n// between them, so the icon and the name are two links to one place.\n// - An image with `height` gets a gray background, so each icon is square and has only `width`.\n// - `align=\"middle\"` is `-webkit-baseline-middle` in Chrome, which is below the text.\n// `align=\"absmiddle\"` is the CSS value `middle`.\nimport { State } from \"alchemy/State\";\nimport * as Effect from \"effect/Effect\";\nimport * as Schema from \"effect/Schema\";\nimport { RUN_RECORD_KEY, runRecord } from \"./RunRecord.ts\";\n\n/** The first text of the comment, which finds it again. */\nconst MARKER = \"<!-- samebase-stacks -->\";\n\n/** GitHub does not proxy this host, so a new icon needs no purge of its image cache. */\nconst ICONS =\n \"https://raw.githubusercontent.com/samebase/samebase/main/packages/alchemy-convex/assets/stack-comment\";\n\nconst GITHUB_API = \"https://api.github.com\";\n\n/** The resources that a card shows. A row of any other type, or without `attr` from an interrupted deploy, is left out. */\nconst Resource = Schema.Union([\n Schema.Struct({\n resourceType: Schema.Literal(\"Convex.Deployment\"),\n attr: Schema.Struct({ name: Schema.String, siteUrl: Schema.String }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"Cloudflare.Worker\"),\n attr: Schema.Struct({ workerName: Schema.String, url: Schema.optional(Schema.String) }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"Cloudflare.R2.Bucket\"),\n attr: Schema.Struct({\n bucketName: Schema.String,\n accountId: Schema.String,\n jurisdiction: Schema.String,\n }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"IdentityHub.SharedClient\"),\n attr: Schema.Struct({ status: Schema.Literals([\"on\", \"off\", \"suspended\"]) }),\n }),\n]);\ntype Resource = typeof Resource.Type;\n\n/** The fields of the run record that a card shows. */\nconst StageOutput = Schema.Struct({\n [RUN_RECORD_KEY]: Schema.Struct({\n headCommit: Schema.String,\n runUrl: Schema.NullOr(Schema.String),\n recordedAt: Schema.String,\n }),\n});\n\nexport interface StackCard {\n readonly name: string;\n readonly stage: string;\n readonly resources: readonly Resource[];\n readonly run: typeof StageOutput.Type | null;\n}\n\nexport class StackCommentError extends Schema.TaggedError<StackCommentError>()(\n \"StackCommentError\",\n { reason: Schema.String },\n) {\n override get message() {\n return `The stack comment cannot be updated: ${this.reason}`;\n }\n}\n\n/** The cards of the stacks of `repository` that have `stage`, in name order. */\nexport const readStackCards = ({ repository, stage }: { repository: string; stage: string }) =>\n Effect.gen(function* () {\n const state = yield* yield* State;\n const names = (yield* state.listStacks()).filter(\n (name) => name === repository || name.startsWith(`${repository}/`),\n );\n const cards: StackCard[] = [];\n for (const name of names) {\n if (!(yield* state.listStages(name)).includes(stage)) {\n continue;\n }\n const request = { stack: name, stage };\n const rows = yield* Effect.forEach(yield* state.list(request), (fqn) =>\n state.get({ ...request, fqn }),\n );\n const output = yield* state.getOutput(request);\n cards.push({\n name,\n stage,\n resources: rows.filter(Schema.is(Resource)),\n run: Schema.is(StageOutput)(output) ? output : null,\n });\n }\n return cards;\n });\n\n/** A stack can create a deployment in one resource and push to it in another: a card shows it once. */\nconst withoutRepeats = (resources: readonly Resource[]) => {\n const deployments = new Set<string>();\n return resources.filter((resource) => {\n if (resource.resourceType !== \"Convex.Deployment\") {\n return true;\n }\n const repeated = deployments.has(resource.attr.name);\n deployments.add(resource.attr.name);\n return !repeated;\n });\n};\n\nconst escapeHtml = (text: string) =>\n text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\n\nconst link = (href: string, text: string) => `<a href=\"${escapeHtml(href)}\">${text}</a>`;\n\nconst named = (href: string, icon: string, alt: string, name: string) => {\n const image = `<img src=\"${ICONS}/${icon}.svg\" width=\"16\" align=\"absmiddle\" alt=\"${alt}\">`;\n return `${link(href, image)} ${link(href, escapeHtml(name))}`;\n};\n\nconst hostOf = (url: string) => escapeHtml(new URL(url).host);\n\n/** Where a stack serves: its Convex deployment, else its first Worker with a URL. */\nconst addressOf = (resources: readonly Resource[]) => {\n for (const resource of resources) {\n if (resource.resourceType === \"Convex.Deployment\") {\n return resource.attr.siteUrl;\n }\n }\n for (const resource of resources) {\n if (resource.resourceType === \"Cloudflare.Worker\" && resource.attr.url !== undefined) {\n return resource.attr.url;\n }\n }\n return null;\n};\n\nconst resourceRow = (resource: Resource, address: string | null) => {\n switch (resource.resourceType) {\n case \"Convex.Deployment\": {\n const { name } = resource.attr;\n return named(\n `https://dashboard.convex.dev/d/${encodeURIComponent(name)}`,\n \"convex\",\n \"Convex\",\n name,\n );\n }\n case \"Cloudflare.Worker\": {\n const { url, workerName } = resource.attr;\n const row = named(\n `https://dash.cloudflare.com/?to=/:account/workers/services/view/${encodeURIComponent(workerName)}`,\n \"worker\",\n \"Worker\",\n workerName,\n );\n return url === undefined || url === address\n ? row\n : `${row}<br><sub>${link(url, hostOf(url))}</sub>`;\n }\n case \"Cloudflare.R2.Bucket\": {\n const { accountId, bucketName, jurisdiction } = resource.attr;\n return named(\n `https://dash.cloudflare.com/${encodeURIComponent(accountId)}/r2/${encodeURIComponent(jurisdiction)}/buckets/${encodeURIComponent(bucketName)}`,\n \"r2\",\n \"R2 bucket\",\n bucketName,\n );\n }\n case \"IdentityHub.SharedClient\": {\n const { status } = resource.attr;\n return `Google sign-in: ${status === \"off\" ? \"not set up\" : status}`;\n }\n }\n};\n\nconst runLine = ({ run }: StackCard) => {\n if (run === null) {\n return \"\";\n }\n const { headCommit, recordedAt, runUrl } = run[RUN_RECORD_KEY];\n const commit = escapeHtml(headCommit.slice(0, 7));\n const time = `${recordedAt.slice(0, 16).replace(\"T\", \" \")} UTC`;\n return `<br><sub>${runUrl === null ? commit : link(runUrl, commit)} ${escapeHtml(time)}</sub>`;\n};\n\nconst SPACER = `<div><img src=\"${ICONS}/spacer.svg\" width=\"460\" align=\"left\" alt=\"\"></div>`;\n\n/** `alone`: the one card of the repository. A card at the root then has no folder, as on the repository page. */\nconst renderCard = (card: StackCard, repository: string, alone: boolean) => {\n const folder = card.name.slice(repository.length + 1);\n const heading = [\n folder === \"\" && alone ? \"\" : `<b>${escapeHtml(folder || \"/\")}</b> `,\n `<sub>${escapeHtml(card.stage)}</sub>`,\n ].join(\"\");\n const resources = withoutRepeats(card.resources);\n const address = addressOf(resources);\n const addressLine = address === null ? \"\" : `<br>${link(address, hostOf(address))}`;\n const rows = resources\n .map((resource) => `<tbody><tr><td>${resourceRow(resource, address)}</td></tr></tbody>`)\n .join(\"\\n\");\n return `<table>\n<tbody><tr><td>${SPACER}${heading}${addressLine}${runLine(card)}</td></tr></tbody>\n${rows}\n</table>`;\n};\n\nexport const renderStackComment = (cards: readonly StackCard[], repository: string) => `${MARKER}\n${cards.map((card) => renderCard(card, repository, cards.length === 1)).join(\"\\n\\n\")}\n`;\n\nconst Comments = Schema.Array(Schema.Struct({ id: Schema.Number, body: Schema.String }));\nconst SavedComment = Schema.Struct({ html_url: Schema.String });\n\nconst github = (token: string, path: string, init: { method: string; body?: string }) =>\n Effect.tryPromise({\n try: async (): Promise<unknown> => {\n const response = await fetch(`${GITHUB_API}${path}`, {\n ...init,\n headers: {\n Accept: \"application/vnd.github+json\",\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n });\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n return await response.json();\n },\n catch: (error) =>\n new StackCommentError({\n reason: `GitHub ${init.method} ${path} failed (${error instanceof Error ? error.message : String(error)})`,\n }),\n });\n\nconst decode =\n <S extends Schema.Codec<unknown, unknown>>(schema: S, what: string) =>\n (value: unknown) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError(() => new StackCommentError({ reason: `GitHub gave a bad ${what}` })),\n );\n\nconst findComment = (token: string, path: string) =>\n Effect.gen(function* () {\n for (let page = 1; ; page += 1) {\n const comments = yield* github(token, `${path}?per_page=100&page=${page}`, {\n method: \"GET\",\n }).pipe(Effect.flatMap(decode(Comments, \"comment list\")));\n const comment = comments.find(({ body }) => body.startsWith(MARKER));\n if (comment !== undefined || comments.length < 100) {\n return comment;\n }\n }\n });\n\n/** Replaces the body of the comment of this command, or posts it on the first run, and gives its URL. */\nexport const saveStackComment = (args: {\n readonly token: string;\n readonly repository: string;\n readonly pullRequest: number;\n readonly body: string;\n}) =>\n Effect.gen(function* () {\n const { body, pullRequest, repository, token } = args;\n const comments = `/repos/${repository}/issues/${pullRequest}/comments`;\n const comment = yield* findComment(token, comments);\n const reply = yield* comment === undefined\n ? github(token, comments, { method: \"POST\", body: JSON.stringify({ body }) })\n : github(token, `/repos/${repository}/issues/comments/${comment.id}`, {\n method: \"PATCH\",\n body: JSON.stringify({ body }),\n });\n const saved = yield* decode(SavedComment, \"comment\")(reply);\n return saved.html_url;\n });\n\n/** The step of a workflow: the run record names the repository and the pull request of the run. */\nexport const updateStackComment = Effect.gen(function* () {\n const { pullRequest, repository } = yield* runRecord({ cwd: process.cwd(), env: process.env });\n if (pullRequest === null) {\n return yield* new StackCommentError({ reason: \"this run has no pull request\" });\n }\n const token = process.env[\"GITHUB_TOKEN\"];\n if (token === undefined || token === \"\") {\n return yield* new StackCommentError({ reason: \"GITHUB_TOKEN is not set\" });\n }\n const cards = yield* readStackCards({ repository, stage: `pr-${pullRequest}` });\n return yield* saveStackComment({\n token,\n repository,\n pullRequest,\n body: renderStackComment(cards, repository),\n });\n});\n","#!/usr/bin/env node\n// The commands of the package:\n//\n// alchemy-convex hub-client --team <team> --app <app> --callback-path <path>\n// alchemy-convex stack-comment [--team <team>]\n//\n// hub-client runs in the directory of a Convex app, and gives the development deployment of that\n// directory a client of the identity hub of the team: see registerDevelopmentClient. stack-comment\n// runs as a step of a pull request workflow after the deploys, and keeps one comment with the stacks\n// of the repository: see src/StackComment.ts. It needs GITHUB_TOKEN and, with a team access token,\n// no --team. Both use CONVEX_ACCESS_TOKEN or the Convex CLI login.\nimport { execFileSync } from \"node:child_process\";\nimport { parseArgs } from \"node:util\";\nimport * as NodeServices from \"@effect/platform-node/NodeServices\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport { fromConfig } from \"./Credentials.ts\";\nimport { registerDevelopmentClient } from \"./HubClient.ts\";\nimport { ManagementApiLive } from \"./ManagementApi.ts\";\nimport { installedCli } from \"./Push.ts\";\nimport { updateStackComment } from \"./StackComment.ts\";\nimport { state } from \"./State.ts\";\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n team: { type: \"string\" },\n app: { type: \"string\" },\n \"callback-path\": { type: \"string\" },\n \"env-prefix\": { type: \"string\" },\n },\n});\nconst { team, app, \"callback-path\": callbackPath, \"env-prefix\": envPrefix } = values;\nconst USAGE = `Usage:\n alchemy-convex hub-client --team <team> --app <app> --callback-path <path> [--env-prefix HUB]\n alchemy-convex stack-comment [--team <team>]`;\n\nconst hubClient = (hub: { team: string; app: string; callbackPath: string; envPrefix?: string }) =>\n Effect.gen(function* () {\n const [command, ...prefix] = yield* installedCli(process.cwd());\n const callbackUrl = yield* registerDevelopmentClient({\n ...hub,\n convex: (args, input) =>\n execFileSync(command, [...prefix, ...args], {\n encoding: \"utf8\",\n input,\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n }).trim(),\n });\n console.log(\n callbackUrl === undefined\n ? \"Hub client already registered.\"\n : `Hub client registered for ${callbackUrl}.`,\n );\n }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));\n\nconst stackComment = (options: { team?: string }) =>\n updateStackComment.pipe(\n Effect.tap((url) => Effect.sync(() => console.log(url))),\n Effect.provide(state(options).pipe(Layer.provide(NodeServices.layer))),\n );\n\nconst subcommand = positionals.join(\" \");\nconst program: Effect.Effect<unknown, unknown> | undefined =\n subcommand === \"stack-comment\"\n ? stackComment(team === undefined ? {} : { team })\n : subcommand === \"hub-client\" &&\n team !== undefined &&\n app !== undefined &&\n callbackPath !== undefined\n ? hubClient({ team, app, callbackPath, ...(envPrefix === undefined ? {} : { envPrefix }) })\n : undefined;\nif (program === undefined) {\n console.error(USAGE);\n process.exit(1);\n}\n\nawait Effect.runPromise(program).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/StackComment.ts","../src/cli.ts"],"sourcesContent":["// The command `alchemy-convex stack-comment`: one comment on a pull request with the stacks of the\n// repository, read from the state store after the deploys.\n//\n// A stack sees only itself, so a comment that a stack declares cannot show the other stacks of the\n// repository. This command runs as a step after them and reads each stack named `owner/name` or\n// `owner/name/<folder>` that has the stage `pr-<number>`. A stack without that stage has no card. A\n// run replaces the whole body, so the last run shows the newest state.\n//\n// It reads only the fields of the rows below, because Alchemy state holds secrets in clear text.\n//\n// GitHub keeps a fixed set of HTML in a comment and removes `style` and `class`, so every choice of\n// the markup is a fact about the sanitizer or the CSS of github.com:\n// - A table has `display: block` and `width: max-content`, so a `width` on it does nothing. The\n// spacer, a floated image one pixel high in a block of its own, gives every card one width, and\n// it shrinks on a narrow page.\n// - The CSS colors the even rows of a group, so each row has its own `tbody`.\n// - An HTML block ends at a blank line, so a table has none and is not indented.\n// - A bare image links to its raw file, and one link around an image and text underlines the space\n// between them, so the icon and the name are two links to one place.\n// - An image with `height` gets a gray background, so each icon is square and has only `width`.\n// - `align=\"middle\"` is `-webkit-baseline-middle` in Chrome, which is below the text.\n// `align=\"absmiddle\"` is the CSS value `middle`.\nimport { State } from \"alchemy/State\";\nimport * as Effect from \"effect/Effect\";\nimport * as Schema from \"effect/Schema\";\nimport { RUN_RECORD_KEY, runRecord } from \"./RunRecord.ts\";\n\n/** The first text of the comment, which finds it again. */\nconst MARKER = \"<!-- samebase-stacks -->\";\n\n/** GitHub does not proxy this host, so a new icon needs no purge of its image cache. */\nconst ICONS =\n \"https://raw.githubusercontent.com/samebase/samebase/main/packages/alchemy-convex/assets/stack-comment\";\n\nconst GITHUB_API = \"https://api.github.com\";\n\n/** The resources that a card shows. A row of any other type, or without `attr` from an interrupted deploy, is left out. */\nconst Resource = Schema.Union([\n Schema.Struct({\n resourceType: Schema.Literal(\"Convex.Deployment\"),\n attr: Schema.Struct({ name: Schema.String, siteUrl: Schema.String }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"Cloudflare.Worker\"),\n attr: Schema.Struct({ workerName: Schema.String, url: Schema.optional(Schema.String) }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"Cloudflare.R2.Bucket\"),\n attr: Schema.Struct({\n bucketName: Schema.String,\n accountId: Schema.String,\n jurisdiction: Schema.String,\n }),\n }),\n Schema.Struct({\n resourceType: Schema.Literal(\"IdentityHub.SharedClient\"),\n attr: Schema.Struct({ status: Schema.Literals([\"on\", \"off\", \"suspended\"]) }),\n }),\n]);\ntype Resource = typeof Resource.Type;\n\n/** The fields of the run record that a card shows. */\nconst StageOutput = Schema.Struct({\n [RUN_RECORD_KEY]: Schema.Struct({\n headCommit: Schema.String,\n runUrl: Schema.NullOr(Schema.String),\n recordedAt: Schema.String,\n }),\n});\n\nexport interface StackCard {\n readonly name: string;\n readonly stage: string;\n readonly resources: readonly Resource[];\n readonly run: typeof StageOutput.Type | null;\n}\n\nexport class StackCommentError extends Schema.TaggedError<StackCommentError>()(\n \"StackCommentError\",\n { reason: Schema.String },\n) {\n override get message() {\n return `The stack comment cannot be updated: ${this.reason}`;\n }\n}\n\n/** The cards of the stacks of `repository` that have `stage`, in name order. */\nexport const readStackCards = ({ repository, stage }: { repository: string; stage: string }) =>\n Effect.gen(function* () {\n const state = yield* yield* State;\n const names = (yield* state.listStacks()).filter(\n (name) => name === repository || name.startsWith(`${repository}/`),\n );\n const cards: StackCard[] = [];\n for (const name of names) {\n if (!(yield* state.listStages(name)).includes(stage)) {\n continue;\n }\n const request = { stack: name, stage };\n const rows = yield* Effect.forEach(yield* state.list(request), (fqn) =>\n state.get({ ...request, fqn }),\n );\n const output = yield* state.getOutput(request);\n cards.push({\n name,\n stage,\n resources: rows.filter(Schema.is(Resource)),\n run: Schema.is(StageOutput)(output) ? output : null,\n });\n }\n return cards;\n });\n\n/** A stack can create a deployment in one resource and push to it in another: a card shows it once. */\nconst withoutRepeats = (resources: readonly Resource[]) => {\n const deployments = new Set<string>();\n return resources.filter((resource) => {\n if (resource.resourceType !== \"Convex.Deployment\") {\n return true;\n }\n const repeated = deployments.has(resource.attr.name);\n deployments.add(resource.attr.name);\n return !repeated;\n });\n};\n\nconst escapeHtml = (text: string) =>\n text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\n\nconst link = (href: string, text: string) => `<a href=\"${escapeHtml(href)}\">${text}</a>`;\n\nconst named = (href: string, icon: string, alt: string, name: string) => {\n const image = `<img src=\"${ICONS}/${icon}.svg\" width=\"16\" align=\"absmiddle\" alt=\"${alt}\">`;\n return `${link(href, image)} ${link(href, escapeHtml(name))}`;\n};\n\nconst hostOf = (url: string) => escapeHtml(new URL(url).host);\n\n/** Where a stack serves: its Convex deployment, else its first Worker with a URL. */\nconst addressOf = (resources: readonly Resource[]) => {\n for (const resource of resources) {\n if (resource.resourceType === \"Convex.Deployment\") {\n return resource.attr.siteUrl;\n }\n }\n for (const resource of resources) {\n if (resource.resourceType === \"Cloudflare.Worker\" && resource.attr.url !== undefined) {\n return resource.attr.url;\n }\n }\n return null;\n};\n\nconst resourceRow = (resource: Resource, address: string | null) => {\n switch (resource.resourceType) {\n case \"Convex.Deployment\": {\n const { name } = resource.attr;\n return named(\n `https://dashboard.convex.dev/d/${encodeURIComponent(name)}`,\n \"convex\",\n \"Convex\",\n name,\n );\n }\n case \"Cloudflare.Worker\": {\n const { url, workerName } = resource.attr;\n const row = named(\n `https://dash.cloudflare.com/?to=/:account/workers/services/view/${encodeURIComponent(workerName)}`,\n \"worker\",\n \"Worker\",\n workerName,\n );\n return url === undefined || url === address\n ? row\n : `${row}<br><sub>${link(url, hostOf(url))}</sub>`;\n }\n case \"Cloudflare.R2.Bucket\": {\n const { accountId, bucketName, jurisdiction } = resource.attr;\n return named(\n `https://dash.cloudflare.com/${encodeURIComponent(accountId)}/r2/${encodeURIComponent(jurisdiction)}/buckets/${encodeURIComponent(bucketName)}`,\n \"r2\",\n \"R2 bucket\",\n bucketName,\n );\n }\n case \"IdentityHub.SharedClient\": {\n const { status } = resource.attr;\n return `Google sign-in: ${status === \"off\" ? \"not set up\" : status}`;\n }\n }\n};\n\nconst runLine = ({ run }: StackCard) => {\n if (run === null) {\n return \"\";\n }\n const { headCommit, recordedAt, runUrl } = run[RUN_RECORD_KEY];\n const commit = escapeHtml(headCommit.slice(0, 7));\n const time = `${recordedAt.slice(0, 16).replace(\"T\", \" \")} UTC`;\n return `<br><sub>${runUrl === null ? commit : link(runUrl, commit)} ${escapeHtml(time)}</sub>`;\n};\n\nconst SPACER = `<div><img src=\"${ICONS}/spacer.svg\" width=\"460\" align=\"left\" alt=\"\"></div>`;\n\n/** `alone`: the one card of the repository. A card at the root then has no folder, as on the repository page. */\nconst renderCard = (card: StackCard, repository: string, alone: boolean) => {\n const folder = card.name.slice(repository.length + 1);\n const heading = [\n folder === \"\" && alone ? \"\" : `<b>${escapeHtml(folder || \"/\")}</b> `,\n `<sub>${escapeHtml(card.stage)}</sub>`,\n ].join(\"\");\n const resources = withoutRepeats(card.resources);\n const address = addressOf(resources);\n const addressLine = address === null ? \"\" : `<br>${link(address, hostOf(address))}`;\n const rows = resources\n .map((resource) => `<tbody><tr><td>${resourceRow(resource, address)}</td></tr></tbody>`)\n .join(\"\\n\");\n return `<table>\n<tbody><tr><td>${SPACER}${heading}${addressLine}${runLine(card)}</td></tr></tbody>\n${rows}\n</table>`;\n};\n\nexport const renderStackComment = (cards: readonly StackCard[], repository: string) => `${MARKER}\n${cards.map((card) => renderCard(card, repository, cards.length === 1)).join(\"\\n\\n\")}\n`;\n\nconst Comments = Schema.Array(Schema.Struct({ id: Schema.Number, body: Schema.String }));\nconst SavedComment = Schema.Struct({ html_url: Schema.String });\n\nconst github = (token: string, path: string, init: { method: string; body?: string }) =>\n Effect.tryPromise({\n try: async (): Promise<unknown> => {\n const response = await fetch(`${GITHUB_API}${path}`, {\n ...init,\n headers: {\n Accept: \"application/vnd.github+json\",\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n });\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n return await response.json();\n },\n catch: (error) =>\n new StackCommentError({\n reason: `GitHub ${init.method} ${path} failed (${error instanceof Error ? error.message : String(error)})`,\n }),\n });\n\nconst decode =\n <S extends Schema.Codec<unknown, unknown>>(schema: S, what: string) =>\n (value: unknown) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError(() => new StackCommentError({ reason: `GitHub gave a bad ${what}` })),\n );\n\nconst findComment = (token: string, path: string) =>\n Effect.gen(function* () {\n for (let page = 1; ; page += 1) {\n const comments = yield* github(token, `${path}?per_page=100&page=${page}`, {\n method: \"GET\",\n }).pipe(Effect.flatMap(decode(Comments, \"comment list\")));\n const comment = comments.find(({ body }) => body.startsWith(MARKER));\n if (comment !== undefined || comments.length < 100) {\n return comment;\n }\n }\n });\n\n/** Replaces the body of the comment of this command, or posts it on the first run, and gives its URL. */\nexport const saveStackComment = (args: {\n readonly token: string;\n readonly repository: string;\n readonly pullRequest: number;\n readonly body: string;\n}) =>\n Effect.gen(function* () {\n const { body, pullRequest, repository, token } = args;\n const comments = `/repos/${repository}/issues/${pullRequest}/comments`;\n const comment = yield* findComment(token, comments);\n const reply = yield* comment === undefined\n ? github(token, comments, { method: \"POST\", body: JSON.stringify({ body }) })\n : github(token, `/repos/${repository}/issues/comments/${comment.id}`, {\n method: \"PATCH\",\n body: JSON.stringify({ body }),\n });\n const saved = yield* decode(SavedComment, \"comment\")(reply);\n return saved.html_url;\n });\n\n/** The step of a workflow: the run record names the repository and the pull request of the run. */\nexport const updateStackComment = Effect.gen(function* () {\n const { pullRequest, repository } = yield* runRecord({ cwd: process.cwd(), env: process.env });\n if (pullRequest === null) {\n return yield* new StackCommentError({ reason: \"this run has no pull request\" });\n }\n const token = process.env[\"GITHUB_TOKEN\"];\n if (token === undefined || token === \"\") {\n return yield* new StackCommentError({ reason: \"GITHUB_TOKEN is not set\" });\n }\n const cards = yield* readStackCards({ repository, stage: `pr-${pullRequest}` });\n return yield* saveStackComment({\n token,\n repository,\n pullRequest,\n body: renderStackComment(cards, repository),\n });\n});\n","#!/usr/bin/env node\n// The commands of the package:\n//\n// alchemy-convex hub-client --team <team> --app <app> --callback-path <path>\n// alchemy-convex stack-comment [--team <team>]\n//\n// hub-client runs in the directory of a Convex app at each dev start, and renews the client of the\n// identity hub of the team that the development deployment of that directory has, or gives it a new\n// one: see registerDevelopmentClient. stack-comment runs as a step of a pull request workflow after\n// the deploys, and keeps one comment with the stacks of the repository: see src/StackComment.ts. It\n// needs GITHUB_TOKEN and, with a team access token, no --team. Both use CONVEX_ACCESS_TOKEN or the\n// Convex CLI login.\nimport { execFileSync } from \"node:child_process\";\nimport { parseArgs } from \"node:util\";\nimport * as NodeServices from \"@effect/platform-node/NodeServices\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport { fromConfig } from \"./Credentials.ts\";\nimport { registerDevelopmentClient } from \"./HubClient.ts\";\nimport { ManagementApiLive } from \"./ManagementApi.ts\";\nimport { installedCli } from \"./Push.ts\";\nimport { updateStackComment } from \"./StackComment.ts\";\nimport { state } from \"./State.ts\";\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n team: { type: \"string\" },\n app: { type: \"string\" },\n \"callback-path\": { type: \"string\" },\n \"env-prefix\": { type: \"string\" },\n },\n});\nconst { team, app, \"callback-path\": callbackPath, \"env-prefix\": envPrefix } = values;\nconst USAGE = `Usage:\n alchemy-convex hub-client --team <team> --app <app> --callback-path <path> [--env-prefix HUB]\n alchemy-convex stack-comment [--team <team>]`;\n\nconst hubClient = (hub: { team: string; app: string; callbackPath: string; envPrefix?: string }) =>\n Effect.gen(function* () {\n const [command, ...prefix] = yield* installedCli(process.cwd());\n const callbackUrl = yield* registerDevelopmentClient({\n ...hub,\n convex: (args, input) =>\n execFileSync(command, [...prefix, ...args], {\n encoding: \"utf8\",\n input,\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n }).trim(),\n });\n console.log(\n callbackUrl === undefined\n ? \"Hub client renewed.\"\n : `Hub client registered for ${callbackUrl}.`,\n );\n }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));\n\nconst stackComment = (options: { team?: string }) =>\n updateStackComment.pipe(\n Effect.tap((url) => Effect.sync(() => console.log(url))),\n Effect.provide(state(options).pipe(Layer.provide(NodeServices.layer))),\n );\n\nconst subcommand = positionals.join(\" \");\nconst program: Effect.Effect<unknown, unknown> | undefined =\n subcommand === \"stack-comment\"\n ? stackComment(team === undefined ? {} : { team })\n : subcommand === \"hub-client\" &&\n team !== undefined &&\n app !== undefined &&\n callbackPath !== undefined\n ? hubClient({ team, app, callbackPath, ...(envPrefix === undefined ? {} : { envPrefix }) })\n : undefined;\nif (program === undefined) {\n console.error(USAGE);\n process.exit(1);\n}\n\nawait Effect.runPromise(program).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n});\n"],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -121,7 +121,7 @@ export declare class Providers extends Providers_base {}
121
121
  * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
122
122
  * `CONVEX_DEPLOY_KEY`).
123
123
  */
124
- export declare const providers: (credentials?: Layer.Layer<Credentials>) => Layer.Layer<Providers, never, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
124
+ export declare const providers: (credentials?: Layer.Layer<Credentials>) => Layer.Layer<Providers, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/process/ChildProcessSpawner").ChildProcessSpawner>;
125
125
  //#endregion
126
126
  //#region src/Push.d.ts
127
127
  /** A value of the `env` prop: a string, or a secret such as `Config.Redacted("AUTH_SECRET")`. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Component.ts","../src/Project.ts","../src/HubClient.ts","../src/RunRecord.ts","../src/SharedHubClient.ts","../src/State.ts"],"mappings":";;;;;;;;;;;;;;;;UA6BiB;;WAEN,aAAa,SAAS;;WAEtB;;UAGM;;WAEN,aAAa,OAAO,OAAO;;WAE3B,gBAAgB,OAAO;;WAEvB,WAAW,OAAO,OAAO,SAAS;;;qBAGhC,oBAAoB;;qBAKpB;;qBAGA;;;;qBAOA,yBAAyB;;;;;;;;;qBAiCzB,kBAAiB,MAAM,MAAM;;qBA0B7B,aAAc;WAChB,cAAc,SAAS;WACvB,YAAY,SAAS;MAC5B,MAAM,MAAM;;;KC3GJ,mBAAmB,kBAAkB;KACrC,mBAAmB,kBAAkB;;UAGhC,YAAY;WAClB,OAAO;WACP;WACA,UAAU;;;;;;;;;;;;;qBAQR,uBAAuB;UAuBnB;;WAEN,UAAU,GACjB,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,cACP,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,0BACxC,OAAO,aAAa;;WAEhB,oBAAoB,GAC3B,mBACA,6BACA,WAAW,SAAS,kBACpB,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;;;;;;;WAQb,gBACP,sCACG,OAAO,eAAe;;;;;;;;WAQlB,oBAAoB,mBAAmB,OAAO;;;;;;;WAO9C,mBAAmB,OAAO;;;;;WAK1B,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBC/FtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,+DAAA,kDAAA,mCAAA;;;;KC6BlE,oBAAoB,SAAS;;;;;;KAO7B;;WAIG;;;;;;;;WAQA;;;;;;;WAOA;;;;;;qBAkCF,0BAA0B;MAIxB;;;;;;;qBAMF,0BAA0B;MAOxB;;;;;;;;;qBAMF,uBAAuB;MAMrB;;;;;;;qBAQF,2BAA2B;MAIzB;;;;;;;qBAMF,oCAAoC;MAIlC;;;;;;qBAMF,kCAAkC;MAIhC;;;qBAuCF,oBACX;EACG;EAAa;;;;UC3HR;;WAEC;aAAoB;;;;;;;;WAOpB;;;;;;WAMA,SAAS;;WAET,MAAM,SAAS,eAAe;;KAG7B,mBACP;;WAEU;MAEV;;WAEU;;;;;WAKA;;;UAIL;WACC;WACA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;UAGM,6BAA6B;;WAEnC;;WAEA;;YAGC,aAAa,8BAEvB,iBACA,sBACA,kBACA;;KAQU,kBAAkB,+BAA8B,KACxD,UAAU,kBACR,WAAW;;;;;qBAWJ,wBAAwB;MAGtB;;;;;;;;;;;;qBAeF,cAAU,YACV,OAAS,oBAAe,OAAA,OAAA,mBAAA;;;;;;EAYvB,MAAA,YAAM;IAAc;IAAgB;QAAgB,OAAA,OAAA;;;;;;;;;;qBAMrD,iCAAiC;MAU/B;;;;;;qBAMF,qCAAqC;MAInC;;;;;;;;;qBAMF,4BAA4B;MAS1B;;;;;UC1OE;WACN;WACA;WACA;;;;;;;;;qBAUE,gBAAiB;EAC5B,YAAY;EACZ;EACA,WAAW;MACZ,OAAA,yBAAA;;;;KCWW,mBAAmB;KAEnB;;WAGG;;WAEA;;WAEA,mBAAmB;;WAEnB;;;WAIA;;WAEA;;UAGE;WACN;WACA;WACA;WACA;WACA;WACA;;YAGC,UAAU,2BAA2B,cAAc,0BAA0B;qBAC5E,2BAAO,cAAA;;;;;;;qBAGP,0BAA0B;MAIxB;;;;;;;;;;;;qBASF,gCAAgC;MAS9B;;;;;;;;qBAOF,yBAAyB;MAKvB;;;;;;qBAMF,+BAA+B;MAI7B;;;;;;;qBAMF,wBAAwB;MAItB;;;;;qBC/FF;KAED;UAEK;;WAEN;;WAEA;;WAEA;WACA,gBAAgB;;WAEhB;;WAEA;;UAGM;;WAEN;WACA;WACA,gBAAgB;;WAEhB;WACA;;WAEA,cAAc,SAAS;;YAGtB,YAAY,+BAEtB,gBACA,4BAEA;qBAEW,6BAAS,cAAA;;;;;;qBAGT,wBAAwB;MAItB;;;;;qBCzDF;;UAMI;WACN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;WACA;;WAEA;;;;;qBAGE,uBAAuB;MAGrB;;;UAME;WACN;WACA,KAAK,SAAS;;;;;;;;qBAuIZ,YAAa,OAAO,mBAAiB,OAAO,OAAO,WAAW;;;;;;;qBAiB9D,gBAAiB,iBAAiB,QAAQ;;;UC7KtC;;WAEN;;;;;;WAMA;WACA;;WAEA;;UAGM;;WAEN;;WAEA;WACA;;WAEA,cAAc,SAAS;;WAEvB,yBAAyB,SAAS;WAClC;WACA;;WAEA;;YAGC,kBAAkB,qCAE5B,sBACA,kCAEA;qBAEW,mCAAe,cAAA;;;;qBCsBf;;;;;;;;;qBA8ZA,QACX;WAAoB;MACnB,MAAM,MAAM,cAAc,oBAAoB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Component.ts","../src/Project.ts","../src/HubClient.ts","../src/RunRecord.ts","../src/SharedHubClient.ts","../src/State.ts"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as PushTargetMismatch, B as DEPLOY_KEY_ENV, C as DeploymentProvider, D as ConvexAppNotFound, E as CodePushFailed, F as ManagementApi, H as fromValues, I as ManagementApiLive, L as ACCESS_TOKEN_ENV, N as parseDeployOutput, O as ConvexCliNotFound, P as ConvexApiError, R as Credentials, S as DeploymentIdentityChange, T as ProductionDeploymentNotFound, V as fromConfig, _ as ProjectProvider, b as AmbiguousDeployment, c as RUN_RECORD_KEY, d as withRunRecord, f as AmbiguousProject, g as ProjectNotFound, h as ProjectDeploymentChange, i as HubClientProvider, j as VariablesOwnedBySomeoneElse, k as EnvironmentNeedsDeployKey, l as RunRecordError, m as Project, n as HubClient, o as STORE_PROJECT_NAME, p as CreatedProjectNotFound, r as HubClientFailed, s as state, t as HUB_PROJECT_NAME, u as runRecord, v as ProjectTeamChange, w as DeploymentResource, x as Deployment, y as AbsoluteRootDir, z as CredentialsError } from "./HubClient-B6hjkbjW.js";
1
+ import { A as PushTargetMismatch, B as DEPLOY_KEY_ENV, C as DeploymentProvider, D as ConvexAppNotFound, E as CodePushFailed, F as ManagementApi, H as fromValues, I as ManagementApiLive, L as ACCESS_TOKEN_ENV, N as parseDeployOutput, O as ConvexCliNotFound, P as ConvexApiError, R as Credentials, S as DeploymentIdentityChange, T as ProductionDeploymentNotFound, V as fromConfig, _ as ProjectProvider, b as AmbiguousDeployment, c as RUN_RECORD_KEY, d as withRunRecord, f as AmbiguousProject, g as ProjectNotFound, h as ProjectDeploymentChange, i as HubClientProvider, j as VariablesOwnedBySomeoneElse, k as EnvironmentNeedsDeployKey, l as RunRecordError, m as Project, n as HubClient, o as STORE_PROJECT_NAME, p as CreatedProjectNotFound, r as HubClientFailed, s as state, t as HUB_PROJECT_NAME, u as runRecord, v as ProjectTeamChange, w as DeploymentResource, x as Deployment, y as AbsoluteRootDir, z as CredentialsError } from "./HubClient-Bs5_j18U.js";
2
2
  import { toPath } from "alchemy/FQN";
3
3
  import { Stack } from "alchemy/Stack";
4
4
  import * as Effect from "effect/Effect";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
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",
package/src/HubClient.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  // change of `app` updates the stored props and does not move the client: the
14
14
  // app is the identity of the users of the app, and it does not change. The
15
15
  // team does not change either: the state of a stack is in its team.
16
+ //
17
+ // Each deploy renews the client on the hub, so a preview or development client
18
+ // is deleted 14 days after its last deploy or use. A client that the hub no
19
+ // longer has gets a new one in the same run. A production client never
20
+ // expires, and a destroy leaves it: the production deployment can stay.
16
21
  import { Resource } from "alchemy";
17
22
  import { isResolved } from "alchemy/Diff";
18
23
  import * as Provider from "alchemy/Provider";
@@ -82,6 +87,9 @@ const Registered = Schema.Struct({
82
87
  clientSecret: Schema.NonEmptyString,
83
88
  });
84
89
 
90
+ /** The reply of `identityBroker:renewClient`: false when the hub no longer has the client. */
91
+ const Renewed = Schema.Boolean;
92
+
85
93
  /** Runs `use` with the function API of the hub of `team`, with a key of this call only. */
86
94
  const withHub = <A, E, R>(
87
95
  api: ManagementApiService,
@@ -114,7 +122,7 @@ const runHub = (
114
122
  functions: FunctionApi,
115
123
  team: string | number,
116
124
  name: string,
117
- args: Record<string, string>,
125
+ args: Record<string, string | boolean>,
118
126
  ) =>
119
127
  functions.call(`identityBroker:${name}`, args).pipe(
120
128
  Effect.mapError((error) => new HubClientFailed({ team, reason: error.message })),
@@ -126,8 +134,9 @@ const runHub = (
126
134
  );
127
135
 
128
136
  /**
129
- * Registers one client. An attempt that a lost reply repeats leaves one spare
130
- * client on the hub, which signs nobody in.
137
+ * Registers one client, which expires unless it is a production client. An
138
+ * attempt that a lost reply repeats leaves one spare client on the hub, which
139
+ * signs nobody in, and which the hub deletes after 14 days.
131
140
  */
132
141
  export const registerHubClient = (api: ManagementApiService, props: HubClientProps) =>
133
142
  withHub(api, props.team, (functions) =>
@@ -139,6 +148,7 @@ export const registerHubClient = (api: ManagementApiService, props: HubClientPro
139
148
  softwareVersion: props.softwareVersion ?? "unknown",
140
149
  // Convex Auth's generic OAuth method posts the client credentials in the body.
141
150
  tokenEndpointAuthMethod: "client_secret_post",
151
+ expires: true,
142
152
  }),
143
153
  ).pipe(
144
154
  Effect.flatMap((value) =>
@@ -155,6 +165,35 @@ export const registerHubClient = (api: ManagementApiService, props: HubClientPro
155
165
  })),
156
166
  );
157
167
 
168
+ /**
169
+ * Renews one client: true while the hub has it. A preview or development client
170
+ * then expires 14 days after this call.
171
+ */
172
+ const renewHubClient = (api: ManagementApiService, team: string | number, clientId: string) =>
173
+ withHub(api, team, (functions) =>
174
+ runHub(functions, team, "renewClient", { clientId, expires: true }),
175
+ ).pipe(
176
+ Effect.flatMap((value) =>
177
+ Schema.decodeUnknownEffect(Renewed)(value).pipe(
178
+ Effect.mapError(() => new HubClientFailed({ team, reason: "renewClient gave no answer" })),
179
+ ),
180
+ ),
181
+ );
182
+
183
+ /**
184
+ * Renews a client whose values the caller holds: false only when the hub answers that the client is
185
+ * gone. Any other failure, such as a hub or a Convex API that cannot answer, keeps the client and
186
+ * warns, so an outage never stops a deploy or a dev start; the next one renews it.
187
+ */
188
+ const renewOrKeep = (api: ManagementApiService, team: string | number, clientId: string) =>
189
+ renewHubClient(api, team, clientId).pipe(
190
+ Effect.catch((error) =>
191
+ Effect.logWarning(
192
+ `Convex.HubClient: ${clientId} keeps its saved values, because the hub could not renew it: ${error.message}`,
193
+ ).pipe(Effect.as(true)),
194
+ ),
195
+ );
196
+
158
197
  const unregister = (api: ManagementApiService, team: string | number, clientId: string) =>
159
198
  withHub(api, team, (functions) => runHub(functions, team, "deleteClient", { clientId }));
160
199
 
@@ -168,27 +207,24 @@ export const HubClientProvider = () =>
168
207
  Effect.gen(function* () {
169
208
  const api = yield* ManagementApi;
170
209
  return {
171
- diff: ({ news, olds, output }) =>
172
- Effect.succeed(
173
- // Props that are unresolved at plan time, such as a redirectUri from a deployment that
174
- // this run creates, are compared in reconcile.
175
- !isResolved(news) || output === undefined
176
- ? undefined
177
- : !registeredFor(output, news)
178
- ? ({ action: "replace" } as const)
179
- : olds.team === news.team && olds.app === news.app
180
- ? ({ action: "noop" } as const)
181
- : ({ action: "update" } as const),
182
- ),
210
+ // Always an update, so that each deploy reaches the hub and renews the client. No attribute
211
+ // is stable, so a push reads the values that this run ended with. The same values cause no
212
+ // other push.
213
+ diff: ({ news }) =>
214
+ Effect.succeed(isResolved(news) ? ({ action: "update" } as const) : undefined),
183
215
 
184
216
  read: ({ output }) => Effect.succeed(output),
185
217
 
186
218
  reconcile: Effect.fn(function* ({ news, output }) {
187
- if (output !== undefined && registeredFor(output, news)) return output;
219
+ const saved = output !== undefined && registeredFor(output, news) ? output : undefined;
220
+ if (saved !== undefined && (yield* renewOrKeep(api, news.team, saved.clientId))) {
221
+ return saved;
222
+ }
223
+ // A new client: the hub no longer has the saved one, or the redirect URI or the
224
+ // deployment type changed. In the second case the old client goes, and the new client is
225
+ // returned whatever happens to the old one.
188
226
  const client = yield* registerHubClient(api, news);
189
- // An update whose props were unresolved at plan time registers in place of a
190
- // replacement. The new client is returned whatever happens to the old one.
191
- if (output !== undefined) {
227
+ if (output !== undefined && saved === undefined) {
192
228
  yield* unregister(api, news.team, output.clientId).pipe(
193
229
  Effect.catchTag("HubClientFailed", (error) =>
194
230
  Effect.logWarning(
@@ -205,8 +241,11 @@ export const HubClientProvider = () =>
205
241
  };
206
242
  }),
207
243
 
244
+ // A destroy can leave a production deployment running, so its client stays.
208
245
  delete: ({ olds, output }) =>
209
- unregister(api, olds.team, output.clientId).pipe(Effect.asVoid),
246
+ output.deploymentType === "production"
247
+ ? Effect.void
248
+ : unregister(api, olds.team, output.clientId).pipe(Effect.asVoid),
210
249
  };
211
250
  }),
212
251
  );
@@ -224,13 +263,16 @@ export interface DevelopmentClientOptions {
224
263
 
225
264
  /**
226
265
  * `alchemy-convex hub-client`: a stack deploys production and previews, and this covers the
227
- * development deployment, which no stack deploys. A deployment with all three variables keeps
228
- * them. So a deployment that holds placeholder values for a checkout without a hub is left alone.
229
- * The secret reaches the Convex CLI on standard input, never in its arguments.
266
+ * development deployment, which no stack deploys. Run it at each dev start: it renews the client
267
+ * that the three variables name, so the client expires 14 days after the last dev start or use. A
268
+ * deployment without all three variables, or whose client the hub no longer has, such as one with
269
+ * the `no-hub` values, gets a new client. Answers the callback URL of a new client, or undefined
270
+ * for a renewal. The secret reaches the Convex CLI on standard input, never in its arguments.
230
271
  */
231
272
  export const registerDevelopmentClient = (options: DevelopmentClientOptions) =>
232
273
  Effect.gen(function* () {
233
274
  const { convex } = options;
275
+ const api = yield* ManagementApi;
234
276
  const envPrefix = options.envPrefix ?? "HUB";
235
277
  const variables = [
236
278
  `${envPrefix}_ISSUER`,
@@ -238,7 +280,10 @@ export const registerDevelopmentClient = (options: DevelopmentClientOptions) =>
238
280
  `${envPrefix}_CLIENT_SECRET`,
239
281
  ];
240
282
  const names = convex(["env", "list", "--names-only"]).split("\n");
241
- if (variables.every((name) => names.includes(name))) return undefined;
283
+ if (variables.every((name) => names.includes(name))) {
284
+ const clientId = convex(["env", "get", `${envPrefix}_CLIENT_ID`]);
285
+ if (yield* renewOrKeep(api, options.team, clientId)) return undefined;
286
+ }
242
287
  const siteUrl = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(
243
288
  convex(["run", "--inline-query", "process.env.CONVEX_SITE_URL"]),
244
289
  ).pipe(
@@ -247,7 +292,7 @@ export const registerDevelopmentClient = (options: DevelopmentClientOptions) =>
247
292
  ),
248
293
  );
249
294
  const callbackUrl = `${siteUrl.replace(/\/$/, "")}${options.callbackPath}`;
250
- const client = yield* registerHubClient(yield* ManagementApi, {
295
+ const client = yield* registerHubClient(api, {
251
296
  team: options.team,
252
297
  app: options.app,
253
298
  redirectUri: callbackUrl,
package/src/State.ts CHANGED
@@ -173,7 +173,7 @@ export interface FunctionApi {
173
173
  /** POST /api/function, the call that `convex run` makes: the reply, which can be a function error. */
174
174
  readonly call: (
175
175
  path: string,
176
- args: Record<string, string | RunRecord | undefined>,
176
+ args: Record<string, string | boolean | RunRecord | undefined>,
177
177
  ) => Effect.Effect<Reply, StateStoreError>;
178
178
  }
179
179
 
package/src/cli.ts CHANGED
@@ -4,11 +4,12 @@
4
4
  // alchemy-convex hub-client --team <team> --app <app> --callback-path <path>
5
5
  // alchemy-convex stack-comment [--team <team>]
6
6
  //
7
- // hub-client runs in the directory of a Convex app, and gives the development deployment of that
8
- // directory a client of the identity hub of the team: see registerDevelopmentClient. stack-comment
9
- // runs as a step of a pull request workflow after the deploys, and keeps one comment with the stacks
10
- // of the repository: see src/StackComment.ts. It needs GITHUB_TOKEN and, with a team access token,
11
- // no --team. Both use CONVEX_ACCESS_TOKEN or the Convex CLI login.
7
+ // hub-client runs in the directory of a Convex app at each dev start, and renews the client of the
8
+ // identity hub of the team that the development deployment of that directory has, or gives it a new
9
+ // one: see registerDevelopmentClient. stack-comment runs as a step of a pull request workflow after
10
+ // the deploys, and keeps one comment with the stacks of the repository: see src/StackComment.ts. It
11
+ // needs GITHUB_TOKEN and, with a team access token, no --team. Both use CONVEX_ACCESS_TOKEN or the
12
+ // Convex CLI login.
12
13
  import { execFileSync } from "node:child_process";
13
14
  import { parseArgs } from "node:util";
14
15
  import * as NodeServices from "@effect/platform-node/NodeServices";
@@ -49,7 +50,7 @@ const hubClient = (hub: { team: string; app: string; callbackPath: string; envPr
49
50
  });
50
51
  console.log(
51
52
  callbackUrl === undefined
52
- ? "Hub client already registered."
53
+ ? "Hub client renewed."
53
54
  : `Hub client registered for ${callbackUrl}.`,
54
55
  );
55
56
  }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));