@samebase/alchemy-convex 0.18.0 → 0.19.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.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 = \"https://raw.githubusercontent.com/samebase/alchemy-convex/main/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]);\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 }\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 },\n});\nconst { team, app, \"callback-path\": callbackPath } = values;\nconst USAGE = `Usage:\n alchemy-convex hub-client --team <team> --app <app> --callback-path <path>\n alchemy-convex stack-comment [--team <team>]`;\n\nconst hubClient = (hub: { team: string; app: string; callbackPath: 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 })\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]);\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 }\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 },\n});\nconst { team, app, \"callback-path\": callbackPath } = values;\nconst USAGE = `Usage:\n alchemy-convex hub-client --team <team> --app <app> --callback-path <path>\n alchemy-convex stack-comment [--team <team>]`;\n\nconst hubClient = (hub: { team: string; app: string; callbackPath: 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 })\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 +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/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/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;;;;;WAK9C,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBCzFtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,8CAAA,mCAAA,oDAAA;;;;KC8BlE,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;;;;qBClH1C;;;;;;;;;qBA8ZA,QACX;WAAoB;MACnB,MAAM,MAAM,cAAc,oBAAoB"}
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-BZKweNfD.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-C0XxgkK3.js";
2
2
  import { toPath } from "alchemy/FQN";
3
3
  import { Stack } from "alchemy/Stack";
4
4
  import * as Effect from "effect/Effect";
@@ -0,0 +1,86 @@
1
+ //#region src/store/sealedSecret.ts
2
+ const toBase64 = (bytes) => btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""));
3
+ const toBase64Url = (bytes) => toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
4
+ /** The bytes of base64 or base64url text. */
5
+ const fromBase64 = (text) => {
6
+ const standard = text.replaceAll("-", "+").replaceAll("_", "/");
7
+ const padded = standard.padEnd(standard.length + (4 - standard.length % 4) % 4, "=");
8
+ return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
9
+ };
10
+ /** A key of 32 bytes in base64 is 43 characters and one `=`. */
11
+ const KEY_TEXT = /^[A-Za-z0-9+/]{43}=$/;
12
+ /**
13
+ * The key ring in the text of the variable `name`. It fails closed when the text is missing or
14
+ * malformed, or when the current key id has no key. An error never quotes the text, which holds
15
+ * the keys.
16
+ */
17
+ const parseKeyRing = (name, text) => {
18
+ let value;
19
+ try {
20
+ value = JSON.parse(text ?? "");
21
+ } catch {
22
+ throw new Error(`${name} is missing or is not JSON.`);
23
+ }
24
+ const malformed = /* @__PURE__ */ new Error(`${name} must be {"current":"<keyId>","keys":{"<keyId>":"<base64 of 32 bytes>"}}.`);
25
+ if (typeof value !== "object" || value === null) throw malformed;
26
+ if (!("current" in value) || !("keys" in value)) throw malformed;
27
+ const { current, keys: keyTexts } = value;
28
+ if (typeof current !== "string" || typeof keyTexts !== "object" || keyTexts === null) throw malformed;
29
+ const keys = /* @__PURE__ */ new Map();
30
+ for (const [keyId, key] of Object.entries(keyTexts)) {
31
+ if (typeof key !== "string" || !KEY_TEXT.test(key)) throw malformed;
32
+ keys.set(keyId, fromBase64(key));
33
+ }
34
+ if (!keys.has(current)) throw new Error(`${name} has no key for its current key id.`);
35
+ return {
36
+ current,
37
+ keys
38
+ };
39
+ };
40
+ /** A random key id of 12 hexadecimal characters, which is also safe in a variable name. */
41
+ const newKeyId = () => Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(6)), (byte) => byte.toString(16).padStart(2, "0")).join("");
42
+ /** A random key of 32 bytes in base64. It needs real randomness, like `sealSecret`. */
43
+ const newKey = () => toBase64(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)));
44
+ /** The text of a new key ring with one random key. */
45
+ const newKeyRing = () => {
46
+ const keyId = newKeyId();
47
+ return JSON.stringify({
48
+ current: keyId,
49
+ keys: { [keyId]: newKey() }
50
+ });
51
+ };
52
+ const importKey = (ring, keyId) => {
53
+ const bytes = ring.keys.get(keyId);
54
+ if (bytes === void 0) throw new Error(`The key ring has no key "${keyId}".`);
55
+ return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
56
+ };
57
+ /** Seals `plaintext` with the current key. `aad` names the owner, so the value opens only there. */
58
+ const sealSecret = async (ring, args) => {
59
+ const key = await importKey(ring, ring.current);
60
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
61
+ const ciphertext = await crypto.subtle.encrypt({
62
+ name: "AES-GCM",
63
+ iv,
64
+ additionalData: new TextEncoder().encode(args.aad)
65
+ }, key, new TextEncoder().encode(args.plaintext));
66
+ return {
67
+ keyId: ring.current,
68
+ iv: toBase64Url(iv),
69
+ ciphertext: toBase64Url(new Uint8Array(ciphertext))
70
+ };
71
+ };
72
+ /** Opens a sealed value with the key it names. A different `aad` or a changed value fails. */
73
+ const openSecret = async (ring, args) => {
74
+ const { sealed } = args;
75
+ const key = await importKey(ring, sealed.keyId);
76
+ const plaintext = await crypto.subtle.decrypt({
77
+ name: "AES-GCM",
78
+ iv: fromBase64(sealed.iv),
79
+ additionalData: new TextEncoder().encode(args.aad)
80
+ }, key, fromBase64(sealed.ciphertext));
81
+ return new TextDecoder().decode(plaintext);
82
+ };
83
+ //#endregion
84
+ export { parseKeyRing as a, openSecret as i, newKeyId as n, sealSecret as o, newKeyRing as r, newKey as t };
85
+
86
+ //# sourceMappingURL=sealedSecret-Bf8AeIp_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedSecret-Bf8AeIp_.js","names":[],"sources":["../src/store/sealedSecret.ts"],"sourcesContent":["// AES-256-GCM sealing under a key ring, with Web Crypto only, so that it runs in Convex and in Node.\n//\n// A key ring is the JSON text `{\"current\":\"<keyId>\",\"keys\":{\"<keyId>\":\"<base64 of 32 bytes>\"}}`,\n// which a deployment keeps in one environment variable. `sealSecret` seals with the `current` key.\n// `openSecret` opens with the key that the sealed object names, so an old key stays in the ring\n// until a reseal moves its values to the current key.\n//\n// Seal only where randomness is real: in a Convex action, or outside Convex. In a Convex query or\n// mutation `crypto.getRandomValues` is a seeded deterministic generator, and a repeated IV under one\n// key breaks AES-GCM. Opening draws no randomness, so a query can open.\n\n/** A value sealed with AES-256-GCM under the key `keyId` of a key ring. */\nexport interface SealedSecret {\n readonly keyId: string;\n /** 12 random bytes, base64url. */\n readonly iv: string;\n /** The AES-GCM output with its 16-byte tag, base64url. */\n readonly ciphertext: string;\n}\n\nexport interface KeyRing {\n readonly current: string;\n readonly keys: ReadonlyMap<string, Uint8Array<ArrayBuffer>>;\n}\n\nconst toBase64 = (bytes: Uint8Array) =>\n btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(\"\"));\n\nconst toBase64Url = (bytes: Uint8Array) =>\n toBase64(bytes).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replace(/=+$/, \"\");\n\n/** The bytes of base64 or base64url text. */\nconst fromBase64 = (text: string) => {\n const standard = text.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const padded = standard.padEnd(standard.length + ((4 - (standard.length % 4)) % 4), \"=\");\n return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));\n};\n\n/** A key of 32 bytes in base64 is 43 characters and one `=`. */\nconst KEY_TEXT = /^[A-Za-z0-9+/]{43}=$/;\n\n/**\n * The key ring in the text of the variable `name`. It fails closed when the text is missing or\n * malformed, or when the current key id has no key. An error never quotes the text, which holds\n * the keys.\n */\nexport const parseKeyRing = (name: string, text: string | undefined): KeyRing => {\n let value: unknown;\n try {\n value = JSON.parse(text ?? \"\");\n } catch {\n throw new Error(`${name} is missing or is not JSON.`);\n }\n const malformed = new Error(\n `${name} must be {\"current\":\"<keyId>\",\"keys\":{\"<keyId>\":\"<base64 of 32 bytes>\"}}.`,\n );\n if (typeof value !== \"object\" || value === null) throw malformed;\n if (!(\"current\" in value) || !(\"keys\" in value)) throw malformed;\n const { current, keys: keyTexts } = value;\n if (typeof current !== \"string\" || typeof keyTexts !== \"object\" || keyTexts === null) {\n throw malformed;\n }\n const keys = new Map<string, Uint8Array<ArrayBuffer>>();\n for (const [keyId, key] of Object.entries(keyTexts)) {\n if (typeof key !== \"string\" || !KEY_TEXT.test(key)) throw malformed;\n keys.set(keyId, fromBase64(key));\n }\n if (!keys.has(current)) throw new Error(`${name} has no key for its current key id.`);\n return { current, keys };\n};\n\n/** A random key id of 12 hexadecimal characters, which is also safe in a variable name. */\nexport const newKeyId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(6)), (byte) =>\n byte.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n\n/** A random key of 32 bytes in base64. It needs real randomness, like `sealSecret`. */\nexport const newKey = (): string => toBase64(crypto.getRandomValues(new Uint8Array(32)));\n\n/** The text of a new key ring with one random key. */\nexport const newKeyRing = (): string => {\n const keyId = newKeyId();\n return JSON.stringify({ current: keyId, keys: { [keyId]: newKey() } });\n};\n\nconst importKey = (ring: KeyRing, keyId: string) => {\n const bytes = ring.keys.get(keyId);\n if (bytes === undefined) throw new Error(`The key ring has no key \"${keyId}\".`);\n return crypto.subtle.importKey(\"raw\", bytes, \"AES-GCM\", false, [\"encrypt\", \"decrypt\"]);\n};\n\n/** Seals `plaintext` with the current key. `aad` names the owner, so the value opens only there. */\nexport const sealSecret = async (\n ring: KeyRing,\n args: { readonly plaintext: string; readonly aad: string },\n): Promise<SealedSecret> => {\n const key = await importKey(ring, ring.current);\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv, additionalData: new TextEncoder().encode(args.aad) },\n key,\n new TextEncoder().encode(args.plaintext),\n );\n return {\n keyId: ring.current,\n iv: toBase64Url(iv),\n ciphertext: toBase64Url(new Uint8Array(ciphertext)),\n };\n};\n\n/** Opens a sealed value with the key it names. A different `aad` or a changed value fails. */\nexport const openSecret = async (\n ring: KeyRing,\n args: { readonly sealed: SealedSecret; readonly aad: string },\n): Promise<string> => {\n const { sealed } = args;\n const key = await importKey(ring, sealed.keyId);\n const plaintext = await crypto.subtle.decrypt(\n {\n name: \"AES-GCM\",\n iv: fromBase64(sealed.iv),\n additionalData: new TextEncoder().encode(args.aad),\n },\n key,\n fromBase64(sealed.ciphertext),\n );\n return new TextDecoder().decode(plaintext);\n};\n"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ //#region src/store/sealedSecret.d.ts
2
+ /** A value sealed with AES-256-GCM under the key `keyId` of a key ring. */
3
+ export interface SealedSecret {
4
+ readonly keyId: string;
5
+ /** 12 random bytes, base64url. */
6
+ readonly iv: string;
7
+ /** The AES-GCM output with its 16-byte tag, base64url. */
8
+ readonly ciphertext: string;
9
+ }
10
+ export interface KeyRing {
11
+ readonly current: string;
12
+ readonly keys: ReadonlyMap<string, Uint8Array<ArrayBuffer>>;
13
+ }
14
+ /**
15
+ * The key ring in the text of the variable `name`. It fails closed when the text is missing or
16
+ * malformed, or when the current key id has no key. An error never quotes the text, which holds
17
+ * the keys.
18
+ */
19
+ export declare const parseKeyRing: (name: string, text: string | undefined) => KeyRing;
20
+ /** A random key id of 12 hexadecimal characters, which is also safe in a variable name. */
21
+ export declare const newKeyId: () => string;
22
+ /** A random key of 32 bytes in base64. It needs real randomness, like `sealSecret`. */
23
+ export declare const newKey: () => string;
24
+ /** The text of a new key ring with one random key. */
25
+ export declare const newKeyRing: () => string;
26
+ /** Seals `plaintext` with the current key. `aad` names the owner, so the value opens only there. */
27
+ export declare const sealSecret: (ring: KeyRing, args: {
28
+ readonly plaintext: string;
29
+ readonly aad: string;
30
+ }) => Promise<SealedSecret>;
31
+ /** Opens a sealed value with the key it names. A different `aad` or a changed value fails. */
32
+ export declare const openSecret: (ring: KeyRing, args: {
33
+ readonly sealed: SealedSecret;
34
+ readonly aad: string;
35
+ }) => Promise<string>;
36
+ //#endregion
37
+ //# sourceMappingURL=sealedSecret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedSecret.d.ts","names":[],"sources":["../src/store/sealedSecret.ts"],"mappings":";;iBAYiB;WACN;;WAEA;;WAEA;;iBAGM;WACN;WACA,MAAM,oBAAoB,WAAW;;;;;;;qBAwBnC,eAAgB,cAAc,6BAA2B;;qBA0BzD;;qBAMA;;qBAGA;;qBAYA,aACX,MAAM,SACN;WAAiB;WAA4B;MAC5C,QAAQ;;qBAgBE,aACX,MAAM,SACN;WAAiB,QAAQ;WAAuB;MAC/C"}
@@ -0,0 +1 @@
1
+ export { t as newKey, n as newKeyId, r as newKeyRing, i as openSecret, a as parseKeyRing, o as sealSecret } from "./sealedSecret-Bf8AeIp_.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -9,14 +9,15 @@
9
9
  "effect",
10
10
  "infrastructure-as-code"
11
11
  ],
12
- "homepage": "https://github.com/samebase/alchemy-convex#readme",
12
+ "homepage": "https://github.com/samebase/samebase/tree/main/packages/alchemy-convex#readme",
13
13
  "bugs": {
14
- "url": "https://github.com/samebase/alchemy-convex/issues"
14
+ "url": "https://github.com/samebase/samebase/issues"
15
15
  },
16
16
  "license": "Apache-2.0",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/samebase/alchemy-convex.git"
19
+ "url": "git+https://github.com/samebase/samebase.git",
20
+ "directory": "packages/alchemy-convex"
20
21
  },
21
22
  "bin": {
22
23
  "alchemy-convex": "./lib/cli.js"
@@ -34,22 +35,15 @@
34
35
  ".": {
35
36
  "types": "./lib/index.d.ts",
36
37
  "import": "./lib/index.js"
38
+ },
39
+ "./sealed-secret": {
40
+ "types": "./lib/sealedSecret.d.ts",
41
+ "import": "./lib/sealedSecret.js"
37
42
  }
38
43
  },
39
44
  "publishConfig": {
40
45
  "access": "public"
41
46
  },
42
- "scripts": {
43
- "check": "vp fmt --check && vp lint && pnpm run typecheck && vp test --run",
44
- "typecheck": "tsc --noEmit",
45
- "test": "vp test --run",
46
- "test:live": "node ./scripts/test-live.ts",
47
- "format": "vp fmt",
48
- "lint": "vp lint",
49
- "build": "vp pack",
50
- "prepack": "pnpm run build",
51
- "prepare": "vp config --no-agent"
52
- },
53
47
  "dependencies": {
54
48
  "@convex-dev/platform": "0.1.11"
55
49
  },
@@ -65,8 +59,8 @@
65
59
  "effect": "4.0.5",
66
60
  "publint": "0.3.25",
67
61
  "typescript": "6.0.3",
68
- "vite-plus": "1.1.0",
69
- "vitest": "5.0.3"
62
+ "vite-plus": "1.3.0",
63
+ "vitest": "5.0.5"
70
64
  },
71
65
  "peerDependencies": {
72
66
  "@effect/platform-node": "^4.0.5",
@@ -77,5 +71,12 @@
77
71
  "engines": {
78
72
  "node": ">=22"
79
73
  },
80
- "packageManager": "pnpm@11.28.5"
81
- }
74
+ "scripts": {
75
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
76
+ "typecheck": "tsc --noEmit",
77
+ "test": "pnpm --dir ../.. exec vp test run --project alchemy-convex",
78
+ "test:live": "node ./scripts/test-live.ts",
79
+ "lint": "pnpm --dir ../.. exec vp lint --tsconfig packages/alchemy-convex/tsconfig.json --report-unused-disable-directives packages/alchemy-convex",
80
+ "build": "vp pack"
81
+ }
82
+ }
package/src/Project.ts CHANGED
@@ -249,7 +249,7 @@ export const listProjectsByName = (api: ManagementApiService, teamId: number, na
249
249
  const matches: ProjectDetails[] = [];
250
250
  const seenCursors = new Set<string>();
251
251
  let cursor: string | undefined;
252
- while (true) {
252
+ for (;;) {
253
253
  const body = yield* api.request("list projects", (client) =>
254
254
  client.GET("/teams/{team_id}/projects", {
255
255
  params: {
package/src/Push.ts CHANGED
@@ -458,7 +458,7 @@ const listVariables = (url: string, key: Redacted.Redacted<string>) =>
458
458
  );
459
459
 
460
460
  /** One call that sets variables, or removes those whose value is null. */
461
- const writeVariables = (
461
+ export const writeVariables = (
462
462
  url: string,
463
463
  key: Redacted.Redacted<string>,
464
464
  changes: ReadonlyArray<{ readonly name: string; readonly value: string | null }>,
package/src/RunRecord.ts CHANGED
@@ -188,6 +188,7 @@ export const runRecord = (where: RunEnvironment): Effect.Effect<RunRecord, RunRe
188
188
  where.env["GITHUB_ACTIONS"] === "true" ? fromGitHubActions(where) : fromGit(where);
189
189
 
190
190
  /** A plain object, which a spread keeps whole. A Date, a Duration, or a Redacted value is an object too, and a spread loses it. */
191
+ // eslint-disable-next-line samebase/no-hand-written-type-predicate -- This published package depends on no app package; the body checks the prototype.
191
192
  const isPlainObject = (value: unknown): value is Record<string, unknown> => {
192
193
  if (typeof value !== "object" || value === null) return false;
193
194
  const prototype: unknown = Object.getPrototypeOf(value);
@@ -29,7 +29,8 @@ import { RUN_RECORD_KEY, runRecord } from "./RunRecord.ts";
29
29
  const MARKER = "<!-- samebase-stacks -->";
30
30
 
31
31
  /** GitHub does not proxy this host, so a new icon needs no purge of its image cache. */
32
- const ICONS = "https://raw.githubusercontent.com/samebase/alchemy-convex/main/assets/stack-comment";
32
+ const ICONS =
33
+ "https://raw.githubusercontent.com/samebase/samebase/main/packages/alchemy-convex/assets/stack-comment";
33
34
 
34
35
  const GITHUB_API = "https://api.github.com";
35
36
 
package/src/State.ts CHANGED
@@ -19,6 +19,10 @@
19
19
  // repository-scoped subscriptions with secret values removed. A key that stops working during a
20
20
  // long run (it expires after two hours) is replaced once per call.
21
21
  //
22
+ // The store seals each `Redacted` value in its own functions, with a key in a variable of its
23
+ // deployment, so this client sends and gets clear text. Each run asks whether the sealing is
24
+ // finished, and finishes it: see `ensureSealed`.
25
+ //
22
26
  // Two first runs of a team can both create a project named `alchemy-state-store`.
23
27
  // The run whose project is not the oldest deletes its own, before it writes
24
28
  // anything, and uses the oldest: both runs end on one store.
@@ -75,9 +79,12 @@ import {
75
79
  push,
76
80
  PUSH_KEY_ACTIONS,
77
81
  withPushKey,
82
+ writeVariables,
78
83
  } from "./Push.ts";
79
84
  import { type RunRecord, runRecord, withRunRecord } from "./RunRecord.ts";
85
+ import { stateKeyName } from "./store/convex/sealing.ts";
80
86
  import { STORE_VERSION } from "./store/convex/version.ts";
87
+ import { newKey, newKeyId } from "./store/sealedSecret.ts";
81
88
 
82
89
  /** The name of the state project in each team. */
83
90
  export const STORE_PROJECT_NAME = "alchemy-state-store";
@@ -92,6 +99,9 @@ export const RUN_KEY_ACTIONS = [
92
99
  "deployment:functions:runInternalMutations",
93
100
  ] as const;
94
101
 
102
+ /** The actions of the key of one run on the store: its writes are actions that seal. */
103
+ const STORE_KEY_ACTIONS = [...RUN_KEY_ACTIONS, "deployment:functions:runInternalActions"] as const;
104
+
95
105
  /** The body of POST /api/function, with HTTP 200 for a function error too. */
96
106
  const Reply = Schema.Union([
97
107
  Schema.Struct({ status: Schema.Literal("success"), value: Schema.Unknown }),
@@ -152,6 +162,12 @@ const decode =
152
162
  (value: unknown) =>
153
163
  Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
154
164
 
165
+ /** The reply of `state:sealing`. */
166
+ const Sealing = Schema.Struct({ current: Schema.NullOr(Schema.String), sealed: Schema.Boolean });
167
+
168
+ /** The reply of `state:reseal`. */
169
+ const Resealed = Schema.Struct({ failed: Schema.Array(Schema.String) });
170
+
155
171
  /** The function API of the store deployment, with a deploy key. */
156
172
  export interface FunctionApi {
157
173
  /** POST /api/function, the call that `convex run` makes: the reply, which can be a function error. */
@@ -295,6 +311,41 @@ const pushStore = (
295
311
  );
296
312
  });
297
313
 
314
+ /**
315
+ * Finishes the sealing of the store, whatever an earlier run left: a new key when the store has
316
+ * none, then a reseal unless one finished under the current key. A run reads this each time, so a
317
+ * run that stopped after a push cannot leave rows in clear text. This client makes a key once, and
318
+ * it never reads a key back.
319
+ */
320
+ const ensureSealed = (
321
+ options: StoreOptions,
322
+ deployment: { readonly name: string; readonly deploymentUrl: string },
323
+ functions: FunctionApi,
324
+ ) =>
325
+ Effect.gen(function* () {
326
+ const { current, sealed } = yield* functions
327
+ .call("state:sealing", {})
328
+ .pipe(Effect.flatMap(valueOf), Effect.flatMap(decode(Sealing, "state:sealing")));
329
+ if (current === null) {
330
+ const keyId = newKeyId();
331
+ yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) =>
332
+ writeVariables(deployment.deploymentUrl, key, [
333
+ { name: stateKeyName(keyId), value: newKey() },
334
+ ]),
335
+ );
336
+ yield* functions.call("state:chooseKey", { keyId }).pipe(Effect.flatMap(valueOf));
337
+ }
338
+ if (sealed) return;
339
+ const { failed } = yield* functions
340
+ .call("state:reseal", {})
341
+ .pipe(Effect.flatMap(valueOf), Effect.flatMap(decode(Resealed, "state:reseal")));
342
+ if (failed.length > 0) {
343
+ yield* Effect.logWarning(
344
+ `Convex state: the store could not seal ${failed.join(", ")}. The next run tries again.`,
345
+ );
346
+ }
347
+ });
348
+
298
349
  /**
299
350
  * Creates the state project of the team. Two first runs can both create one:
300
351
  * the run whose project is not the oldest deletes its own, which holds
@@ -357,7 +408,7 @@ export const makeConvexState = (options: StoreOptions) =>
357
408
  }),
358
409
  ),
359
410
  );
360
- const mint = createDeployKey(api, deployment.name, STORE_PROJECT_NAME, RUN_KEY_ACTIONS).pipe(
411
+ const mint = createDeployKey(api, deployment.name, STORE_PROJECT_NAME, STORE_KEY_ACTIONS).pipe(
361
412
  Effect.map((key) => {
362
413
  keys.push(key);
363
414
  return makeFunctionApi(deployment.deploymentUrl, key);
@@ -401,6 +452,8 @@ export const makeConvexState = (options: StoreOptions) =>
401
452
  );
402
453
  }
403
454
 
455
+ yield* ensureSealed(options, deployment, functions);
456
+
404
457
  const remote = remoteOf(functions, options.run);
405
458
  /** The stack's output plus the record, when the run has one and the output has a place for it. */
406
459
  const documentOf = (request: { stack: string; stage: string; value: unknown }) =>
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
 
11
+ import type * as keys from "../keys.js";
11
12
  import type * as reader from "../reader.js";
12
13
  import type * as runRecord from "../runRecord.js";
13
14
  import type * as state from "../state.js";
@@ -20,6 +21,7 @@ import type {
20
21
  import { anyApi, componentsGeneric } from "convex/server";
21
22
 
22
23
  const fullApi: ApiFromModules<{
24
+ keys: typeof keys;
23
25
  reader: typeof reader;
24
26
  runRecord: typeof runRecord;
25
27
  state: typeof state;
@@ -23,6 +23,36 @@ import type { FunctionReference } from "convex/server";
23
23
  */
24
24
  export type ComponentApi<Name extends string | undefined = string | undefined> =
25
25
  {
26
+ keys: {
27
+ choose: FunctionReference<
28
+ "mutation",
29
+ "internal",
30
+ { keyId: string },
31
+ string,
32
+ Name
33
+ >;
34
+ markSealed: FunctionReference<
35
+ "mutation",
36
+ "internal",
37
+ { generation: number },
38
+ null,
39
+ Name
40
+ >;
41
+ rotate: FunctionReference<
42
+ "mutation",
43
+ "internal",
44
+ { keyId: string },
45
+ null,
46
+ Name
47
+ >;
48
+ sealing: FunctionReference<
49
+ "query",
50
+ "internal",
51
+ {},
52
+ { current: string | null; generation: number; sealed: boolean },
53
+ Name
54
+ >;
55
+ };
26
56
  reader: {
27
57
  repository: FunctionReference<
28
58
  "query",
@@ -76,6 +106,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
76
106
  Array<string>,
77
107
  Name
78
108
  >;
109
+ outputPage: FunctionReference<
110
+ "query",
111
+ "internal",
112
+ { cursor: string | null },
113
+ {
114
+ continueCursor: string;
115
+ isDone: boolean;
116
+ page: Array<{ stack: string; stage: string; value: string }>;
117
+ },
118
+ Name
119
+ >;
79
120
  readStack: FunctionReference<
80
121
  "query",
81
122
  "internal",
@@ -133,7 +174,50 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
133
174
  "query",
134
175
  "internal",
135
176
  { stack: string; stage: string },
136
- Array<string>,
177
+ Array<{ fqn: string; value: string }>,
178
+ Name
179
+ >;
180
+ replaceOutput: FunctionReference<
181
+ "mutation",
182
+ "internal",
183
+ {
184
+ expected: string;
185
+ generation: number;
186
+ stack: string;
187
+ stage: string;
188
+ value: string;
189
+ },
190
+ "replaced" | "changed" | "stale",
191
+ Name
192
+ >;
193
+ replaceResource: FunctionReference<
194
+ "mutation",
195
+ "internal",
196
+ {
197
+ expected: string;
198
+ fqn: string;
199
+ generation: number;
200
+ stack: string;
201
+ stage: string;
202
+ value: string;
203
+ },
204
+ "replaced" | "changed" | "stale",
205
+ Name
206
+ >;
207
+ resourcePage: FunctionReference<
208
+ "query",
209
+ "internal",
210
+ { cursor: string | null },
211
+ {
212
+ continueCursor: string;
213
+ isDone: boolean;
214
+ page: Array<{
215
+ fqn: string;
216
+ stack: string;
217
+ stage: string;
218
+ value: string;
219
+ }>;
220
+ },
137
221
  Name
138
222
  >;
139
223
  set: FunctionReference<