-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add webhook commands #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import * as v from "valibot"; | ||
|
|
||
| import { env } from "../lib/env"; | ||
| import { request } from "../lib/request"; | ||
|
|
||
| const WebhookSchema = v.object({ | ||
| config: v.object({ | ||
| _id: v.string(), | ||
| url: v.string(), | ||
| active: v.boolean(), | ||
| name: v.nullable(v.string()), | ||
| secret: v.nullable(v.string()), | ||
| headers: v.record(v.string(), v.string()), | ||
| documentsPublished: v.boolean(), | ||
| documentsUnpublished: v.boolean(), | ||
| releasesCreated: v.boolean(), | ||
| releasesUpdated: v.boolean(), | ||
| tagsCreated: v.boolean(), | ||
| tagsDeleted: v.boolean(), | ||
| }), | ||
| }); | ||
| type Webhook = v.InferOutput<typeof WebhookSchema>; | ||
|
|
||
| export async function getWebhooks(config: { | ||
| repo: string; | ||
| token: string | undefined; | ||
| host: string | undefined; | ||
| }): Promise<Webhook[]> { | ||
| const { repo, token, host } = config; | ||
| const wroomUrl = getWroomUrl(repo, host); | ||
| const url = new URL("app/settings/webhooks", wroomUrl); | ||
| const response = await request(url, { | ||
| credentials: { "prismic-auth": token }, | ||
| schema: v.array(WebhookSchema), | ||
| }); | ||
| return response; | ||
| } | ||
|
|
||
| export async function triggerWebhook( | ||
| id: string, | ||
| config: { repo: string; token: string | undefined; host: string | undefined }, | ||
| ): Promise<void> { | ||
| const { repo, token, host } = config; | ||
| const wroomUrl = getWroomUrl(repo, host); | ||
| const url = new URL(`app/settings/webhooks/${id}/trigger`, wroomUrl); | ||
| await request(url, { | ||
| method: "POST", | ||
| credentials: { "prismic-auth": token }, | ||
| }); | ||
| } | ||
|
|
||
| export async function createWebhook( | ||
| webhookConfig: Omit<Webhook["config"], "_id" | "active" | "headers">, | ||
| config: { repo: string; token: string | undefined; host: string | undefined }, | ||
| ): Promise<void> { | ||
| const { repo, token, host } = config; | ||
| const wroomUrl = getWroomUrl(repo, host); | ||
| const url = new URL(`app/settings/webhooks/create`, wroomUrl); | ||
| const body = new FormData(); | ||
| body.set("url", webhookConfig.url); | ||
| body.set("name", webhookConfig.name ?? ""); | ||
| body.set("secret", webhookConfig.secret ?? ""); | ||
| body.set("headers", JSON.stringify({})); | ||
| body.set("active", "on"); | ||
| body.set("documentsPublished", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("documentsUnpublished", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("releasesCreated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("releasesUpdated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("tagsCreated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("documentsPublished", webhookConfig.documentsUnpublished.toString()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate key overwrites documentsPublished, tagsDeleted never setHigh Severity In both Additional Locations (1) |
||
| await request(url, { | ||
| method: "POST", | ||
| body, | ||
| credentials: { "prismic-auth": token }, | ||
| }); | ||
| } | ||
|
|
||
| export async function updateWebhook( | ||
| id: string, | ||
| webhookConfig: Omit<Webhook["config"], "_id">, | ||
| config: { repo: string; token: string | undefined; host: string | undefined }, | ||
| ): Promise<void> { | ||
| const { repo, token, host } = config; | ||
| const wroomUrl = getWroomUrl(repo, host); | ||
| const url = new URL(`app/settings/webhooks/${id}`, wroomUrl); | ||
| const body = new FormData(); | ||
| body.set("url", webhookConfig.url); | ||
| body.set("name", webhookConfig.name ?? ""); | ||
| body.set("secret", webhookConfig.secret ?? ""); | ||
| body.set("headers", JSON.stringify(webhookConfig.headers ?? {})); | ||
| body.set("active", webhookConfig.active ? "on" : "off"); | ||
| body.set("documentsPublished", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("documentsUnpublished", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("releasesCreated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("releasesUpdated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("tagsCreated", webhookConfig.documentsUnpublished.toString()); | ||
| body.set("documentsPublished", webhookConfig.documentsUnpublished.toString()); | ||
| await request(url, { | ||
| method: "POST", | ||
| body, | ||
| credentials: { "prismic-auth": token }, | ||
| }); | ||
| } | ||
|
|
||
| export async function deleteWebhook( | ||
| id: string, | ||
| config: { repo: string; token: string | undefined; host: string | undefined }, | ||
| ): Promise<void> { | ||
| const { repo, token, host } = config; | ||
| const wroomUrl = getWroomUrl(repo, host); | ||
| const url = new URL(`app/settings/webhooks/${id}/delete`, wroomUrl); | ||
| await request(url, { | ||
| method: "POST", | ||
| credentials: { "prismic-auth": token }, | ||
| }); | ||
| } | ||
|
|
||
| function getWroomUrl(repo: string, host = env.PRISMIC_HOST): URL { | ||
| return new URL(`https://${repo}.${host}/`); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { parseArgs } from "node:util"; | ||
|
|
||
| import { getWebhooks, updateWebhook } from "./clients/wroom"; | ||
| import { getHost, getToken } from "./lib/auth"; | ||
| import { safeGetRepositoryFromConfig } from "./lib/config"; | ||
| import { UnknownRequestError } from "./lib/request"; | ||
|
|
||
| const HELP = ` | ||
| Add a custom HTTP header to a webhook. | ||
|
|
||
| By default, this command reads the repository from prismic.config.json at the | ||
| project root. | ||
|
|
||
| USAGE | ||
| prismic webhook add-header <url> <key> <value> [flags] | ||
|
|
||
| ARGUMENTS | ||
| <url> Webhook URL | ||
| <key> Header name | ||
| <value> Header value | ||
|
|
||
| FLAGS | ||
| -r, --repo string Repository domain | ||
| -h, --help Show help for command | ||
|
|
||
| LEARN MORE | ||
| Use \`prismic <command> <subcommand> --help\` for more information about a command. | ||
| `.trim(); | ||
|
|
||
| export async function webhookAddHeader(): Promise<void> { | ||
| const { | ||
| values: { help, repo = await safeGetRepositoryFromConfig() }, | ||
| positionals: [webhookUrl, headerKey, headerValue], | ||
| } = parseArgs({ | ||
| args: process.argv.slice(4), // skip: node, script, "webhook", "add-header" | ||
| options: { | ||
| repo: { type: "string", short: "r" }, | ||
| help: { type: "boolean", short: "h" }, | ||
| }, | ||
| allowPositionals: true, | ||
| }); | ||
|
|
||
| if (help) { | ||
| console.info(HELP); | ||
| return; | ||
| } | ||
|
|
||
| if (!webhookUrl) { | ||
| console.error("Missing required argument: <url>"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!headerKey) { | ||
| console.error("Missing required argument: <key>"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!headerValue) { | ||
| console.error("Missing required argument: <value>"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!repo) { | ||
| console.error("Missing prismic.config.json or --repo option"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| const token = await getToken(); | ||
| const host = await getHost(); | ||
| const webhooks = await getWebhooks({ repo, token, host }); | ||
| const webhook = webhooks.find((w) => w.config.url === webhookUrl); | ||
| if (!webhook) { | ||
| console.error(`Webhook not found: ${webhookUrl}`); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| const id = webhook.config._id; | ||
|
|
||
| const updatedConfig = structuredClone(webhook.config); | ||
| updatedConfig.headers[headerKey] = headerValue; | ||
|
|
||
| try { | ||
| await updateWebhook(id, updatedConfig, { repo, token, host }); | ||
| } catch (error) { | ||
| if (error instanceof UnknownRequestError) { | ||
| const message = await error.text(); | ||
| console.error(`Failed to add header: ${message}`); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| console.info(`Header added: ${headerKey}`); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import { parseArgs } from "node:util"; | ||
|
|
||
| import { createWebhook } from "./clients/wroom"; | ||
| import { getHost, getToken } from "./lib/auth"; | ||
| import { safeGetRepositoryFromConfig } from "./lib/config"; | ||
| import { UnknownRequestError } from "./lib/request"; | ||
| import { TRIGGER_DISPLAY } from "./webhook-view"; | ||
|
|
||
| const HELP = ` | ||
| Create a new webhook in a Prismic repository. | ||
|
|
||
| By default, this command reads the repository from prismic.config.json at the | ||
| project root. | ||
|
|
||
| USAGE | ||
| prismic webhook create <url> [flags] | ||
|
|
||
| ARGUMENTS | ||
| <url> Webhook URL to receive events | ||
|
|
||
| FLAGS | ||
| -n, --name string Webhook name | ||
| -s, --secret string Secret for webhook signature | ||
| -t, --trigger string Trigger events (can be repeated) | ||
| -r, --repo string Repository domain | ||
| -h, --help Show help for command | ||
|
|
||
| TRIGGERS | ||
| document.published When documents are published | ||
| document.unpublished When documents are unpublished | ||
| release.created When a release is created | ||
| release.updated When a release is edited or deleted | ||
| tag.created When a tag is created | ||
| tag.deleted When a tag is deleted | ||
|
|
||
| If no triggers specified, all are enabled. | ||
|
|
||
| LEARN MORE | ||
| Use \`prismic <command> <subcommand> --help\` for more information about a command. | ||
| `.trim(); | ||
|
|
||
| const VALID_TRIGGERS = Object.values(TRIGGER_DISPLAY); | ||
|
|
||
| export async function webhookCreate(): Promise<void> { | ||
| const { | ||
| values: { help, repo = await safeGetRepositoryFromConfig(), name, secret, trigger = [] }, | ||
| positionals: [webhookUrl], | ||
| } = parseArgs({ | ||
| args: process.argv.slice(4), // skip: node, script, "webhook", "create" | ||
| options: { | ||
| name: { type: "string", short: "n" }, | ||
| secret: { type: "string", short: "s" }, | ||
| trigger: { type: "string", multiple: true, short: "t" }, | ||
| repo: { type: "string", short: "r" }, | ||
| help: { type: "boolean", short: "h" }, | ||
| }, | ||
| allowPositionals: true, | ||
| }); | ||
|
|
||
| if (help) { | ||
| console.info(HELP); | ||
| return; | ||
| } | ||
|
|
||
| if (!webhookUrl) { | ||
| console.error("Missing required argument: <url>"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!repo) { | ||
| console.error("Missing prismic.config.json or --repo option"); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| // Validate triggers | ||
| for (const t of trigger) { | ||
| if (!VALID_TRIGGERS.includes(t)) { | ||
| console.error(`Invalid trigger: ${t}`); | ||
| console.error(`Valid triggers: ${VALID_TRIGGERS.join(", ")}`); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| // Build trigger settings | ||
| const defaultValue = trigger.length > 0 ? false : true; | ||
| const triggers: Record<keyof typeof TRIGGER_DISPLAY, boolean> = { | ||
| documentsPublished: defaultValue, | ||
| documentsUnpublished: defaultValue, | ||
| releasesCreated: defaultValue, | ||
| releasesUpdated: defaultValue, | ||
| tagsCreated: defaultValue, | ||
| tagsDeleted: defaultValue, | ||
| }; | ||
| for (const t of trigger) { | ||
| const [apiField] = Object.entries(TRIGGER_DISPLAY).find(([, display]) => t === display) ?? []; | ||
| if (!apiField) continue; | ||
| triggers[apiField as keyof typeof TRIGGER_DISPLAY] = true; | ||
| } | ||
|
|
||
| const token = await getToken(); | ||
| const host = await getHost(); | ||
|
|
||
| try { | ||
| await createWebhook( | ||
| { | ||
| url: webhookUrl, | ||
| name: name ?? null, | ||
| secret: secret ?? null, | ||
| ...triggers, | ||
| }, | ||
| { repo, token, host }, | ||
| ); | ||
| } catch (error) { | ||
| if (error instanceof UnknownRequestError) { | ||
| const message = await error.text(); | ||
| console.error(`Failed to create webhook: ${message}`); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| console.info(`Webhook created: ${webhookUrl}`); | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All trigger fields use wrong property value
High Severity
In both
createWebhookandupdateWebhook, every trigger field reads its value fromwebhookConfig.documentsUnpublishedinstead of the corresponding property. For example,documentsPublisheduseswebhookConfig.documentsUnpublished,releasesCreateduseswebhookConfig.documentsUnpublished, etc. This means all triggers are always set to whatever thedocumentsUnpublishedvalue is, completely ignoring the caller's intended per-trigger settings.Additional Locations (1)
src/clients/wroom.ts#L91-L97