@samebase/alchemy-convex 0.26.0 → 0.27.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/lib/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { F as installedCli, W as fromConfig, c as registerDevelopmentClient, d as RUN_RECORD_KEY, p as runRecord, r as registerSharedDevelopmentClient, u as state, z as ManagementApiLive } from "./SharedHubClient-D-FJJUCZ.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";
@@ -221,54 +221,37 @@ const { positionals, values } = parseArgs({
221
221
  team: { type: "string" },
222
222
  app: { type: "string" },
223
223
  "callback-path": { type: "string" },
224
- "env-prefix": { type: "string" },
225
- shared: { type: "boolean" },
226
- stack: { type: "string" }
224
+ "env-prefix": { type: "string" }
227
225
  }
228
226
  });
229
- const { team, app, "callback-path": callbackPath, "env-prefix": envPrefix, shared, stack } = values;
227
+ const { team, app, "callback-path": callbackPath, "env-prefix": envPrefix } = values;
230
228
  const USAGE = `Usage:
231
229
  alchemy-convex hub-client --team <team> --app <app> --callback-path <path> [--env-prefix HUB]
232
- alchemy-convex hub-client --shared --stack <owner/name> --app <app> --callback-path <path> [--env-prefix HUB] [--team <team>]
233
230
  alchemy-convex stack-comment [--team <team>]`;
234
- /** The installed Convex CLI on the deployment of this directory. Secrets go on standard input. */
235
- const convexOfThisDirectory = installedCli(process.cwd()).pipe(Effect.map(([command, ...prefix]) => (args, input) => execFileSync(command, [...prefix, ...args], {
236
- encoding: "utf8",
237
- input,
238
- stdio: [
239
- "pipe",
240
- "pipe",
241
- "inherit"
242
- ]
243
- }).trim()));
244
- const managementApi = ManagementApiLive().pipe(Layer.provide(fromConfig()));
245
231
  const hubClient = (hub) => Effect.gen(function* () {
232
+ const [command, ...prefix] = yield* installedCli(process.cwd());
246
233
  const callbackUrl = yield* registerDevelopmentClient({
247
234
  ...hub,
248
- convex: yield* convexOfThisDirectory
235
+ convex: (args, input) => execFileSync(command, [...prefix, ...args], {
236
+ encoding: "utf8",
237
+ input,
238
+ stdio: [
239
+ "pipe",
240
+ "pipe",
241
+ "inherit"
242
+ ]
243
+ }).trim()
249
244
  });
250
245
  console.log(callbackUrl === void 0 ? "Hub client renewed." : `Hub client registered for ${callbackUrl}.`);
251
- }).pipe(Effect.provide(managementApi));
252
- const sharedHubClient = (hub) => convexOfThisDirectory.pipe(Effect.flatMap((convex) => registerSharedDevelopmentClient({
253
- ...hub,
254
- cwd: process.cwd(),
255
- convex
256
- })), Effect.catchTag("ConvexCliNotFound", (error) => Effect.succeed(error.message)), Effect.tap((line) => Effect.sync(() => console.log(line))), Effect.provide(managementApi));
246
+ }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
257
247
  const stackComment = (options) => updateStackComment.pipe(Effect.tap((url) => Effect.sync(() => console.log(url))), Effect.provide(state(options).pipe(Layer.provide(NodeServices.layer))));
258
- const prefix = envPrefix === void 0 ? {} : { envPrefix };
259
248
  const subcommand = positionals.join(" ");
260
- const program = subcommand === "stack-comment" ? stackComment(team === void 0 ? {} : { team }) : subcommand !== "hub-client" || app === void 0 || callbackPath === void 0 ? void 0 : shared === true ? stack === void 0 ? void 0 : sharedHubClient({
261
- stack,
262
- app,
263
- callbackPath,
264
- ...prefix,
265
- ...team === void 0 ? {} : { team }
266
- }) : team === void 0 ? void 0 : hubClient({
249
+ const program = subcommand === "stack-comment" ? stackComment(team === void 0 ? {} : { team }) : subcommand === "hub-client" && team !== void 0 && app !== void 0 && callbackPath !== void 0 ? hubClient({
267
250
  team,
268
251
  app,
269
252
  callbackPath,
270
- ...prefix
271
- });
253
+ ...envPrefix === void 0 ? {} : { envPrefix }
254
+ }) : void 0;
272
255
  if (program === void 0) {
273
256
  console.error(USAGE);
274
257
  process.exit(1);
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> [--env-prefix HUB]\n// alchemy-convex hub-client --shared --stack <owner/name> --app <app> --callback-path <path>\n// [--env-prefix HUB] [--team <team>]\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. With --shared, the client is on the central identity hub, and\n// the initial access token of the stack's production client is the proof: see\n// registerSharedDevelopmentClient. It never stops a dev start. stack-comment runs as a step of a pull\n// request workflow after the deploys, and keeps one comment with the stacks of the repository: see\n// src/StackComment.ts. It needs GITHUB_TOKEN and, with a team access token, no --team. All use\n// 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 { registerSharedDevelopmentClient } from \"./SharedHubClient.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 shared: { type: \"boolean\" },\n stack: { type: \"string\" },\n },\n});\nconst { team, app, \"callback-path\": callbackPath, \"env-prefix\": envPrefix, shared, stack } = values;\nconst USAGE = `Usage:\n alchemy-convex hub-client --team <team> --app <app> --callback-path <path> [--env-prefix HUB]\n alchemy-convex hub-client --shared --stack <owner/name> --app <app> --callback-path <path> [--env-prefix HUB] [--team <team>]\n alchemy-convex stack-comment [--team <team>]`;\n\n/** The installed Convex CLI on the deployment of this directory. Secrets go on standard input. */\nconst convexOfThisDirectory = installedCli(process.cwd()).pipe(\n Effect.map(\n ([command, ...prefix]) =>\n (args: readonly string[], input?: string) =>\n execFileSync(command, [...prefix, ...args], {\n encoding: \"utf8\",\n input,\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n }).trim(),\n ),\n);\n\nconst managementApi = ManagementApiLive().pipe(Layer.provide(fromConfig()));\n\nconst hubClient = (hub: { team: string; app: string; callbackPath: string; envPrefix?: string }) =>\n Effect.gen(function* () {\n const callbackUrl = yield* registerDevelopmentClient({\n ...hub,\n convex: yield* convexOfThisDirectory,\n });\n console.log(\n callbackUrl === undefined\n ? \"Hub client renewed.\"\n : `Hub client registered for ${callbackUrl}.`,\n );\n }).pipe(Effect.provide(managementApi));\n\nconst sharedHubClient = (hub: {\n stack: string;\n app: string;\n callbackPath: string;\n envPrefix?: string;\n team?: string;\n}) =>\n convexOfThisDirectory.pipe(\n Effect.flatMap((convex) =>\n registerSharedDevelopmentClient({ ...hub, cwd: process.cwd(), convex }),\n ),\n // Without the Convex CLI, the dev start goes on too.\n Effect.catchTag(\"ConvexCliNotFound\", (error) => Effect.succeed(error.message)),\n Effect.tap((line) => Effect.sync(() => console.log(line))),\n Effect.provide(managementApi),\n );\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 prefix = envPrefix === undefined ? {} : { envPrefix };\nconst subcommand = positionals.join(\" \");\nconst program: Effect.Effect<unknown, unknown> | undefined =\n subcommand === \"stack-comment\"\n ? stackComment(team === undefined ? {} : { team })\n : subcommand !== \"hub-client\" || app === undefined || callbackPath === undefined\n ? undefined\n : shared === true\n ? stack === undefined\n ? undefined\n : sharedHubClient({\n stack,\n app,\n callbackPath,\n ...prefix,\n ...(team === undefined ? {} : { team }),\n })\n : team === undefined\n ? undefined\n : hubClient({ team, app, callbackPath, ...prefix });\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> [--env-prefix HUB]\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
@@ -10,6 +10,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
10
10
  import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
11
11
  import "effect/Scope";
12
12
  import { State, StateStoreError } from "alchemy/State";
13
+ import * as Output from "alchemy/Output";
13
14
  import { NamespaceNode } from "alchemy/Namespace";
14
15
  import { InputProps } from "alchemy/Input";
15
16
  import { components } from "@convex-dev/platform/managementApi";
@@ -502,44 +503,80 @@ export declare const runRecord: (where: RunEnvironment) => Effect.Effect<RunReco
502
503
  export declare const withRunRecord: (output: unknown, record: RunRecord) => object | undefined;
503
504
  //#endregion
504
505
  //#region src/SharedHubClient.d.ts
505
- interface SharedHubClientProps {
506
+ interface CommonProps {
506
507
  /** The stable name of the app, set once in the stack. */
507
508
  readonly app: string;
508
509
  /**
509
510
  * The return address of the hub's sign-ins, such as `${site.siteUrl}/auth/hub/callback`: an https
510
511
  * URL whose origin serves `/samebase-verification.json` with the numeric GitHub id of this
511
- * repository, `{ "repository": "123" }`. It can come from a resource that this run creates.
512
+ * repository, `{ "repository": "123" }`. It can come from a resource that this run creates. A
513
+ * development client can also have http on a loopback host, which needs no file.
512
514
  */
513
515
  readonly redirectUri: string;
514
- readonly deploymentType: "production" | "preview";
515
516
  /** Recorded on the hub's client, such as the commit. The hub records "unknown" without it. */
516
517
  readonly softwareVersion?: string;
517
518
  }
518
- interface SharedHubClientAttributes {
519
+ /**
520
+ * A production or preview client asks with the GitHub OIDC token of the run. A development client
521
+ * asks with the app's initial access token, which `SharedHubClient.read` gives from stage `prod`.
522
+ */
523
+ type SharedHubClientProps = (CommonProps & {
524
+ readonly deploymentType: "production" | "preview";
525
+ readonly initialAccessToken?: never;
526
+ }) | (CommonProps & {
527
+ readonly deploymentType: "development";
528
+ /** Empty before the first production registration: the client gets the off values. */
529
+ readonly initialAccessToken: Redacted.Redacted<string>;
530
+ });
531
+ declare const Attributes: Schema.Struct<{
519
532
  /** `off`: no client, and the values below are the off values. */
520
- readonly status: "on" | "off" | "suspended";
533
+ readonly status: Schema.Literals<readonly ["on", "off", "suspended"]>;
521
534
  /** The issuer of the hub, which the deployment names to sign in. */
522
- readonly issuer: string;
523
- readonly clientId: string;
535
+ readonly issuer: Schema.String;
536
+ readonly clientId: Schema.String;
524
537
  /** Returned only at registration. */
525
- readonly clientSecret: Redacted.Redacted<string>;
538
+ readonly clientSecret: Schema.Redacted<Schema.String>;
526
539
  /** Returned only at registration. It reads and deletes this client on the hub, and nothing else. */
527
- readonly registrationAccessToken: Redacted.Redacted<string>;
528
- readonly registrationClientUri: string;
529
- readonly deploymentType: "production" | "preview";
540
+ readonly registrationAccessToken: Schema.Redacted<Schema.String>;
541
+ readonly registrationClientUri: Schema.String;
542
+ readonly deploymentType: Schema.Literals<readonly ["production", "preview", "development"]>;
530
543
  /** The return address that the client serves. Empty for the off values. */
531
- readonly redirectUri: string;
544
+ readonly redirectUri: Schema.String;
532
545
  /**
533
546
  * Returned only at a production registration (RFC 7591 section 1.2). It registers development
534
- * clients of this app, such as `alchemy-convex hub-client --shared` on a laptop. Empty for a
535
- * preview and for the off values.
547
+ * clients of this app. Empty for a preview, for a development client, and for the off values.
536
548
  */
537
- readonly initialAccessToken: Redacted.Redacted<string>;
549
+ readonly initialAccessToken: Schema.Redacted<Schema.String>;
538
550
  /** The numeric GitHub id of the repository, with the initial access token. Empty without it. */
539
- readonly repository: string;
540
- }
551
+ readonly repository: Schema.String;
552
+ }>;
553
+ /**
554
+ * The attributes keep the real values of a suspended client, so that an unblock needs no new client.
555
+ * `SharedHubClient.variables` gives the values that the app gets.
556
+ */
557
+ interface SharedHubClientAttributes extends Schema.Schema.Type<typeof Attributes> {}
541
558
  export type SharedHubClient = Resource<"IdentityHub.SharedClient", SharedHubClientProps, SharedHubClientAttributes, never, Providers>;
542
- export declare const SharedHubClient: import("alchemy").ResourceClass<SharedHubClient>;
559
+ export declare const SharedHubClient: import("alchemy").ResourceClassWithMethods<SharedHubClient, {
560
+ readonly read: (id: string, options: {
561
+ readonly stage: string;
562
+ }) => Output.ObjectExpr<{
563
+ readonly status: "on" | "off" | "suspended";
564
+ readonly issuer: string;
565
+ readonly clientId: string;
566
+ readonly clientSecret: Redacted.Redacted<string>;
567
+ readonly registrationAccessToken: Redacted.Redacted<string>;
568
+ readonly registrationClientUri: string;
569
+ readonly deploymentType: "preview" | "production" | "development";
570
+ readonly redirectUri: string;
571
+ readonly initialAccessToken: Redacted.Redacted<string>;
572
+ readonly repository: string;
573
+ }, Stack | State>;
574
+ readonly variables: (client: SharedHubClient) => Output.ObjectExpr<{
575
+ issuer: string;
576
+ clientId: string;
577
+ clientSecret: Redacted.Redacted<string>;
578
+ }, never>;
579
+ }>;
543
580
  //#endregion
544
581
  //#region src/State.d.ts
545
582
  /** The name of the state project in each team. */
@@ -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":"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"}
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,10 +1,16 @@
1
- import { A as ConvexAppNotFound, B as ACCESS_TOKEN_ENV, C as AmbiguousDeployment, D as DeploymentResource, E as DeploymentProvider, G as fromValues, H as CredentialsError, I as parseDeployOutput, L as ConvexApiError, M as EnvironmentNeedsDeployKey, N as PushTargetMismatch, O as ProductionDeploymentNotFound, P as VariablesOwnedBySomeoneElse, R as ManagementApi, S as AbsoluteRootDir, T as DeploymentIdentityChange, U as DEPLOY_KEY_ENV, V as Credentials, W as fromConfig, _ as Project, a as HubClient, b as ProjectProvider, d as RUN_RECORD_KEY, f as RunRecordError, g as CreatedProjectNotFound, h as AmbiguousProject, i as HUB_PROJECT_NAME, j as ConvexCliNotFound, k as CodePushFailed, l as STORE_PROJECT_NAME, m as withRunRecord, n as SharedHubClientProvider, o as HubClientFailed, p as runRecord, s as HubClientProvider, t as SharedHubClient, u as state, v as ProjectDeploymentChange, w as Deployment, x as ProjectTeamChange, y as ProjectNotFound, z as ManagementApiLive } from "./SharedHubClient-D-FJJUCZ.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";
5
5
  import * as Schema from "effect/Schema";
6
6
  import * as Layer from "effect/Layer";
7
+ import * as Option from "effect/Option";
8
+ import * as Redacted from "effect/Redacted";
9
+ import { Resource } from "alchemy";
10
+ import { isResolved } from "alchemy/Diff";
7
11
  import * as Provider from "alchemy/Provider";
12
+ import { State } from "alchemy/State";
13
+ import * as Output from "alchemy/Output";
8
14
  //#region src/Component.ts
9
15
  /**
10
16
  * Associate a native namespace with an installed component. `component` is the
@@ -30,6 +36,267 @@ const bindComponent = (options) => Effect.gen(function* () {
30
36
  });
31
37
  });
32
38
  //#endregion
39
+ //#region src/SharedHubClient.ts
40
+ /** A hung service must not hold a deploy: the time limit of one request. */
41
+ const REQUEST_TIMEOUT_MS = 3e4;
42
+ /** The variables of a deployment without a hub client, as the starter writes them. */
43
+ const OFF_VALUES = {
44
+ issuer: "http://127.0.0.1:1",
45
+ clientId: "no-hub",
46
+ clientSecret: "no-hub"
47
+ };
48
+ const DeploymentType = Schema.Literals([
49
+ "production",
50
+ "preview",
51
+ "development"
52
+ ]);
53
+ const Attributes = Schema.Struct({
54
+ /** `off`: no client, and the values below are the off values. */
55
+ status: Schema.Literals([
56
+ "on",
57
+ "off",
58
+ "suspended"
59
+ ]),
60
+ /** The issuer of the hub, which the deployment names to sign in. */
61
+ issuer: Schema.String,
62
+ clientId: Schema.String,
63
+ /** Returned only at registration. */
64
+ clientSecret: Schema.Redacted(Schema.String),
65
+ /** Returned only at registration. It reads and deletes this client on the hub, and nothing else. */
66
+ registrationAccessToken: Schema.Redacted(Schema.String),
67
+ registrationClientUri: Schema.String,
68
+ deploymentType: DeploymentType,
69
+ /** The return address that the client serves. Empty for the off values. */
70
+ redirectUri: Schema.String,
71
+ /**
72
+ * Returned only at a production registration (RFC 7591 section 1.2). It registers development
73
+ * clients of this app. Empty for a preview, for a development client, and for the off values.
74
+ */
75
+ initialAccessToken: Schema.Redacted(Schema.String),
76
+ /** The numeric GitHub id of the repository, with the initial access token. Empty without it. */
77
+ repository: Schema.String
78
+ });
79
+ /**
80
+ * The attributes of a saved row, or undefined when the row is not of this version, such as a row
81
+ * that 0.26 or earlier saved without the token fields. The text of a schema issue can quote a value
82
+ * of the row, so it is never kept.
83
+ */
84
+ const savedAttributes = (value) => Schema.decodeUnknownEffect(Attributes)(value).pipe(Effect.map((attributes) => attributes), Effect.catchCause(() => Effect.succeed(void 0)));
85
+ /** The attributes of no client. */
86
+ const offAttributes = (deploymentType) => ({
87
+ status: "off",
88
+ issuer: OFF_VALUES.issuer,
89
+ clientId: OFF_VALUES.clientId,
90
+ clientSecret: Redacted.make(OFF_VALUES.clientSecret),
91
+ registrationAccessToken: Redacted.make(""),
92
+ registrationClientUri: "",
93
+ deploymentType,
94
+ redirectUri: "",
95
+ initialAccessToken: Redacted.make(""),
96
+ repository: ""
97
+ });
98
+ /** One request gave no usable answer. The reason holds no secret. */
99
+ var SharedHubClientFailed = class extends Schema.TaggedError()("SharedHubClientFailed", { reason: Schema.String }) {};
100
+ const OidcToken = Schema.Struct({ value: Schema.NonEmptyString });
101
+ /** The 201 answer of `POST /hub-clients`. */
102
+ const NewClient = Schema.Struct({
103
+ issuer: Schema.NonEmptyString,
104
+ client_id: Schema.NonEmptyString,
105
+ client_secret: Schema.NonEmptyString,
106
+ registration_access_token: Schema.NonEmptyString,
107
+ registration_client_uri: Schema.NonEmptyString,
108
+ redirect_uri: Schema.NonEmptyString,
109
+ /**
110
+ * The terms of the shared sign-in service, which the log shows once, for the new client. Only a
111
+ * page of samebase.com: the log line must not print a value that the answer chose.
112
+ */
113
+ terms: Schema.String.check(Schema.isPattern(/^https:\/\/samebase\.com\/[a-z0-9/-]*$/)),
114
+ /** Only in the answer to a production registration with the GitHub token. */
115
+ initial_access_token: Schema.optionalKey(Schema.NonEmptyString),
116
+ repository: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^\d+$/)))
117
+ });
118
+ /** The other answers of `POST /hub-clients`, by status. */
119
+ const REFUSALS = {
120
+ 400: "the identity hub refused the request: the return address must be https on a DNS name, and the app name at most 64 characters",
121
+ 401: "the identity hub refused the GitHub OIDC token",
122
+ 403: "the identity hub does not allow this repository or return address",
123
+ 404: "the identity hub has no /hub-clients",
124
+ 429: "the identity hub limits new clients",
125
+ 502: "GitHub's keys are not available to the identity hub"
126
+ };
127
+ /** The reasons of a 403 that a log line shows. Any other text of the answer is never logged. */
128
+ const NOT_ALLOWED = /* @__PURE__ */ new Map([
129
+ ["paused", "the identity hub pauses new clients of third-party apps"],
130
+ ["run_not_allowed", "the identity hub gives clients only to runs of main and of pull requests"],
131
+ ["blocked", "Samebase blocked this repository or its owner"],
132
+ ["suspended", "the identity hub suspended this repository"],
133
+ ["too_many_apps", "the repository has as many apps on the identity hub as it may"],
134
+ ["too_many_clients", "the app has as many clients on the identity hub as it may"],
135
+ ["app_taken", "the app's name belongs to another app on the identity hub"],
136
+ ["host_unreachable", "the identity hub could not reach the host of the return address"],
137
+ ["verification_file_missing", "the host of the return address does not serve /samebase-verification.json"],
138
+ ["verification_file_invalid", "/samebase-verification.json on the host holds no repository id"],
139
+ ["repository_mismatch", "/samebase-verification.json on the host names another repository"],
140
+ ["app_mismatch", "the initial access token belongs to another app on the identity hub"]
141
+ ]);
142
+ /** The windows of a 429, by the name that the answer gives. */
143
+ const RATE_LIMITED = /* @__PURE__ */ new Map([
144
+ ["repository", "the identity hub gives one repository at most 30 new clients an hour"],
145
+ ["owner", "the identity hub gives one owner at most 100 new clients an hour"],
146
+ ["new_owners", "the identity hub takes at most 50 new owners an hour"]
147
+ ]);
148
+ const Refusal = Schema.Struct({ reason: Schema.String });
149
+ const RateLimited = Schema.Struct({ window: Schema.String });
150
+ /** The text of a refusal: a known reason of a 403 or window of a 429, else the text of its status. */
151
+ const refusalOf = (status, body) => {
152
+ return (status === 403 ? NOT_ALLOWED.get(Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason ?? "") : status === 429 ? RATE_LIMITED.get(Option.getOrUndefined(Schema.decodeUnknownOption(RateLimited)(body))?.window ?? "") : void 0) ?? REFUSALS[status] ?? "the identity hub refused the request";
153
+ };
154
+ const send = (who, url, token, init) => Effect.tryPromise({
155
+ try: async () => {
156
+ const response = await fetch(url, {
157
+ ...init,
158
+ headers: {
159
+ Authorization: `Bearer ${token}`,
160
+ "Content-Type": "application/json"
161
+ },
162
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
163
+ });
164
+ const body = await response.json().catch(() => void 0);
165
+ return {
166
+ status: response.status,
167
+ body
168
+ };
169
+ },
170
+ catch: () => new SharedHubClientFailed({ reason: `${who} gave no answer` })
171
+ });
172
+ const decode = (schema, who) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => new SharedHubClientFailed({ reason: `${who} gave a bad answer` })));
173
+ /** The OIDC token of this run, read right before the call, because it lives for minutes only. */
174
+ const readOidcToken = (requestUrl, requestToken) => Effect.gen(function* () {
175
+ const { body, status } = yield* send("GitHub", `${requestUrl}&audience=samebase`, requestToken, { method: "GET" });
176
+ if (status !== 200) return yield* new SharedHubClientFailed({ reason: `GitHub gave no OIDC token (HTTP ${status})` });
177
+ return (yield* decode(OidcToken, "GitHub")(body)).value;
178
+ });
179
+ /** `POST /hub-clients` with `bearer`: the GitHub OIDC token, or the app's initial access token. */
180
+ const requestNewClient = (props, bearer) => Effect.gen(function* () {
181
+ const { body, status } = yield* send("The identity hub", `${process.env["HUB_URL"] ?? "https://auth.samebase.com"}/hub-clients`, bearer, {
182
+ method: "POST",
183
+ body: JSON.stringify({
184
+ app: props.app,
185
+ redirectUri: props.redirectUri,
186
+ softwareVersion: props.softwareVersion ?? "unknown"
187
+ })
188
+ });
189
+ if (status !== 201) return yield* new SharedHubClientFailed({ reason: `${status === 401 && props.deploymentType === "development" ? "the identity hub refused the initial access token" : refusalOf(status, body)} (HTTP ${status})` });
190
+ const client = yield* decode(NewClient, "The identity hub")(body);
191
+ if (client.redirect_uri !== props.redirectUri) return yield* new SharedHubClientFailed({ reason: "The identity hub gave a bad answer" });
192
+ yield* Effect.logInfo(`Convex.SharedHubClient: ${props.app} on ${props.redirectUri} has a new client of the identity hub. Its use is under ${client.terms}.`);
193
+ const production = props.deploymentType === "production";
194
+ return {
195
+ status: "on",
196
+ issuer: client.issuer,
197
+ clientId: client.client_id,
198
+ clientSecret: Redacted.make(client.client_secret),
199
+ registrationAccessToken: Redacted.make(client.registration_access_token),
200
+ registrationClientUri: client.registration_client_uri,
201
+ deploymentType: props.deploymentType,
202
+ redirectUri: props.redirectUri,
203
+ initialAccessToken: Redacted.make(production ? client.initial_access_token ?? "" : ""),
204
+ repository: production ? client.repository ?? "" : ""
205
+ };
206
+ });
207
+ /** A new client, or the off values. */
208
+ const newClient = (props) => {
209
+ const label = `Convex.SharedHubClient: ${props.app} on ${props.redirectUri} deploys without the hub`;
210
+ const off = offAttributes(props.deploymentType);
211
+ const request = (bearer) => bearer.pipe(Effect.flatMap((token) => requestNewClient(props, token)), Effect.catchTag("SharedHubClientFailed", (error) => Effect.logWarning(`${label}: ${error.reason}. The next deploy asks again.`).pipe(Effect.as(off))));
212
+ if (props.deploymentType === "development") {
213
+ const token = Redacted.value(props.initialAccessToken);
214
+ return token === "" ? Effect.logInfo("Convex.SharedHubClient: deploy to main once for Google sign-in on this laptop.").pipe(Effect.as(off)) : request(Effect.succeed(token));
215
+ }
216
+ const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"];
217
+ const requestToken = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"];
218
+ if (!requestUrl || !requestToken) return (process.env["GITHUB_ACTIONS"] === "true" ? Effect.logWarning(`${label}: this workflow needs the permission id-token: write.`) : Effect.logInfo(`${label}: this run has no GitHub OIDC token.`)).pipe(Effect.as(off));
219
+ return request(readOidcToken(requestUrl, requestToken));
220
+ };
221
+ /** The client after a read on the hub, or undefined when the hub says that the client is gone. */
222
+ const renew = (saved) => Effect.gen(function* () {
223
+ const { status } = yield* send("The identity hub", saved.registrationClientUri, Redacted.value(saved.registrationAccessToken), { method: "GET" });
224
+ switch (status) {
225
+ case 200: return {
226
+ ...saved,
227
+ status: "on"
228
+ };
229
+ case 401: return;
230
+ case 403:
231
+ yield* Effect.logWarning(`Convex.SharedHubClient: the hub suspended the client of ${saved.redirectUri}. It keeps its values, and nobody can sign in with it.`);
232
+ return {
233
+ ...saved,
234
+ status: "suspended"
235
+ };
236
+ default: return yield* new SharedHubClientFailed({ reason: `The identity hub answered HTTP ${status}` });
237
+ }
238
+ }).pipe(Effect.catchTag("SharedHubClientFailed", (error) => Effect.logWarning(`Convex.SharedHubClient: ${saved.redirectUri} keeps the saved client: ${error.reason}.`).pipe(Effect.as(saved))));
239
+ /** A saved row of this resource, whose `attr` `savedAttributes` reads. */
240
+ const SavedRow = Schema.Struct({
241
+ resourceType: Schema.Literal("IdentityHub.SharedClient"),
242
+ attr: Schema.Unknown
243
+ });
244
+ /**
245
+ * The attributes of the client `id` in another stage of this stack, such as
246
+ * `read("HubClient", { stage: "prod" })` in the stage of a laptop, as Outputs: the
247
+ * `initialAccessToken` and the `repository` of the production client. Without that row, the off
248
+ * attributes, with `deploymentType: "production"`.
249
+ *
250
+ * `SharedHubClient.ref` is not enough: Alchemy fails the plan with `InvalidReferenceError` when the
251
+ * row is missing, and a laptop before the first deploy to main has none. This read runs when the
252
+ * plan resolves it, with the State service of the running stack and its `get`, so it never creates
253
+ * or upgrades a store. The store revives each secret as `Redacted`, as it does for every read.
254
+ */
255
+ const read = (id, options) => Output.fromEffect(Effect.gen(function* () {
256
+ const stack = yield* Stack;
257
+ const row = yield* (yield* yield* State).get({
258
+ stack: stack.name,
259
+ stage: options.stage,
260
+ fqn: id
261
+ }).pipe(Effect.orDie);
262
+ if (row === void 0) return offAttributes("production");
263
+ const { attr } = yield* Schema.decodeUnknownEffect(SavedRow)(row).pipe(Effect.catchCause(() => Effect.die(new SharedHubClientFailed({ reason: `the row ${id} of stage ${options.stage} is not a Convex.SharedHubClient` }))));
264
+ const saved = yield* savedAttributes(attr);
265
+ if (saved === void 0) return yield* Effect.die(new SharedHubClientFailed({ reason: `the row ${id} of stage ${options.stage} holds attributes that this version cannot read` }));
266
+ return saved;
267
+ }));
268
+ /**
269
+ * The three values that the app's deployment gets, such as `HUB_ISSUER`, `HUB_CLIENT_ID`, and
270
+ * `HUB_CLIENT_SECRET`, as Outputs: the client's values when `status` is `on`, else the off values,
271
+ * so that a suspended client gives guest sign-in and keeps its values in state.
272
+ */
273
+ const variables = (client) => Output.map(Output.of(client), (attributes) => attributes.status === "on" ? {
274
+ issuer: attributes.issuer,
275
+ clientId: attributes.clientId,
276
+ clientSecret: attributes.clientSecret
277
+ } : {
278
+ issuer: OFF_VALUES.issuer,
279
+ clientId: OFF_VALUES.clientId,
280
+ clientSecret: Redacted.make(OFF_VALUES.clientSecret)
281
+ });
282
+ const SharedHubClient = Resource("IdentityHub.SharedClient")({
283
+ read,
284
+ variables
285
+ });
286
+ const SharedHubClientProvider = () => Provider.succeed(SharedHubClient, {
287
+ diff: ({ news }) => Effect.succeed(isResolved(news) ? { action: "update" } : void 0),
288
+ read: ({ output }) => Effect.succeed(output),
289
+ reconcile: Effect.fn(function* ({ news, output }) {
290
+ const saved = output === void 0 || output.status === "off" ? void 0 : yield* savedAttributes(output);
291
+ if (output !== void 0 && output.status !== "off" && saved === void 0) yield* Effect.logWarning(`Convex.SharedHubClient: ${news.app} on ${news.redirectUri} has a saved client of an older version, so this run asks for a new one.`);
292
+ return (saved?.redirectUri === news.redirectUri && saved.deploymentType === news.deploymentType ? yield* renew(saved) : void 0) ?? (yield* newClient(news));
293
+ }),
294
+ delete: ({ output }) => output.deploymentType === "production" || output.status === "off" ? Effect.void : Effect.gen(function* () {
295
+ const { status } = yield* send("The identity hub", output.registrationClientUri, Redacted.value(output.registrationAccessToken), { method: "DELETE" });
296
+ if (status !== 204 && status !== 401) return yield* new SharedHubClientFailed({ reason: `The identity hub answered HTTP ${status}` });
297
+ }).pipe(Effect.catchTag("SharedHubClientFailed", (error) => Effect.logWarning(`Convex.SharedHubClient: the client of ${output.redirectUri} stays on the hub until its removal date: ${error.reason}.`)))
298
+ });
299
+ //#endregion
33
300
  //#region src/Providers.ts
34
301
  var Providers = class extends Provider.ProviderCollection()("Convex") {};
35
302
  /**