chore: auto-fix lint & formatting [skip ci]

This commit is contained in:
github-actions[bot] 2026-08-24 14:52:59 +00:00
parent a77ac8ad91
commit df16dc94f7
17 changed files with 142 additions and 125 deletions

View file

@ -17,19 +17,19 @@ bun run dev # Start Nuxt dev server (HMR + Nitro)
## Scripts ## Scripts
| Command | Description | | Command | Description |
| ---------------------- | ---------------------------------------- | | ---------------------- | ------------------------------------ |
| `bun run dev` | Start Nuxt dev server | | `bun run dev` | Start Nuxt dev server |
| `bun run build` | Production build (cloudflare preset) | | `bun run build` | Production build (cloudflare preset) |
| `bunx wrangler dev` | Preview a built worker (Miniflare) | | `bunx wrangler dev` | Preview a built worker (Miniflare) |
| `bun run typecheck` | TypeScript type checking | | `bun run typecheck` | TypeScript type checking |
| `bun run lint` | ESLint (TypeScript) | | `bun run lint` | ESLint (TypeScript) |
| `bun run lint:md` | Markdownlint | | `bun run lint:md` | Markdownlint |
| `bun test` | Run the unit-test suite | | `bun test` | Run the unit-test suite |
| `bun run format` | Format all files with Prettier | | `bun run format` | Format all files with Prettier |
| `bun run format:check` | Check Prettier formatting | | `bun run format:check` | Check Prettier formatting |
| `bun run docs:dev` | Start the VitePress docs dev server | | `bun run docs:dev` | Start the VitePress docs dev server |
| `bun run db:migrate` | Apply D1 migrations locally | | `bun run db:migrate` | Apply D1 migrations locally |
## Code Style ## Code Style

View file

@ -5,10 +5,10 @@ import { FILTER_TYPES, FILTER_OPS } from "~/types";
defineOptions({ name: "FilterNodeEditor" }); defineOptions({ name: "FilterNodeEditor" });
const props = withDefaults( const props = withDefaults(defineProps<{ node: NodeForm; depth?: number; deletable?: boolean }>(), {
defineProps<{ node: NodeForm; depth?: number; deletable?: boolean }>(), depth: 0,
{ depth: 0, deletable: false }, deletable: false,
); });
const emit = defineEmits<{ (e: "remove"): void }>(); const emit = defineEmits<{ (e: "remove"): void }>();

View file

@ -3,7 +3,13 @@ import { computed, reactive, ref, watch } from "vue";
import type { Filter, FilterNode, NamedFragment, Route, RouteTarget, RouteTemplate } from "~/types"; import type { Filter, FilterNode, NamedFragment, Route, RouteTarget, RouteTemplate } from "~/types";
import { FRAGMENT_PRESETS, ROUTE_TEMPLATES } from "~/types"; import { FRAGMENT_PRESETS, ROUTE_TEMPLATES } from "~/types";
import type { NodeForm } from "~/composables/useFilterNode"; import type { NodeForm } from "~/composables/useFilterNode";
import { blankLeafForm, blankNode, nodeToForm, nodeFormToRouteFilters, formToNode } from "~/composables/useFilterNode"; import {
blankLeafForm,
blankNode,
nodeToForm,
nodeFormToRouteFilters,
formToNode,
} from "~/composables/useFilterNode";
interface TargetForm { interface TargetForm {
platform: "discord" | "telegram"; platform: "discord" | "telegram";
@ -392,11 +398,7 @@ watch(
<section class="editor-section"> <section class="editor-section">
<h3 class="editor-section-title">{{ t("routeEditor.testMatch") }}</h3> <h3 class="editor-section-title">{{ t("routeEditor.testMatch") }}</h3>
<div class="field"> <div class="field">
<input <input v-model="testEvent" class="input" :placeholder="t('routeEditor.testEvent')" />
v-model="testEvent"
class="input"
:placeholder="t('routeEditor.testEvent')"
/>
</div> </div>
<div class="field"> <div class="field">
<textarea <textarea
@ -413,7 +415,11 @@ watch(
</div> </div>
<div v-if="testResult" class="test-result" :class="{ ok: testResult.matched }"> <div v-if="testResult" class="test-result" :class="{ ok: testResult.matched }">
<span class="test-badge"> <span class="test-badge">
{{ testResult.matched ? t("routeEditor.testMatched") : t("routeEditor.testNotMatched") }} {{
testResult.matched
? t("routeEditor.testMatched")
: t("routeEditor.testNotMatched")
}}
</span> </span>
<span class="test-explanation">{{ testResult.explanation }}</span> <span class="test-explanation">{{ testResult.explanation }}</span>
</div> </div>

View file

@ -38,7 +38,9 @@ function remove(index: number) {
</script> </script>
<template> <template>
<div class="flex flex-wrap items-center gap-1 rounded-md border border-border bg-surface/60 px-2 py-1.5 focus-within:border-accent"> <div
class="flex flex-wrap items-center gap-1 rounded-md border border-border bg-surface/60 px-2 py-1.5 focus-within:border-accent"
>
<span <span
v-for="(v, i) in modelValue" v-for="(v, i) in modelValue"
:key="`${v}-${i}`" :key="`${v}-${i}`"

View file

@ -146,8 +146,7 @@ export function describeLeaf(f: Filter, t: (key: string) => string): string {
const op = f.op ?? "eq"; const op = f.op ?? "eq";
const match = f.match; const match = f.match;
const values = match === undefined ? [] : Array.isArray(match) ? match : [match]; const values = match === undefined ? [] : Array.isArray(match) ? match : [match];
const value = const value = values.map((v) => JSON.stringify(v)).join(` ${t("filterNode.or")} `) || "\u2205";
values.map((v) => JSON.stringify(v)).join(` ${t("filterNode.or")} `) || "\u2205";
let base: string; let base: string;
if (f.type === "keyword") { if (f.type === "keyword") {
base = `${label} ${t("filterNode.matches")} ${value}`; base = `${label} ${t("filterNode.matches")} ${value}`;
@ -169,9 +168,7 @@ export function describeNode(node: FilterNode, t: (key: string) => string): stri
.join(` ${t("filterNode.and")} `); .join(` ${t("filterNode.and")} `);
} }
if ("any" in node) { if ("any" in node) {
const parts = node.any const parts = node.any.map((c) => describeNode(c, t)).filter((s) => s.length);
.map((c) => describeNode(c, t))
.filter((s) => s.length);
return parts.length ? `(${parts.join(` ${t("filterNode.or")} `)})` : ""; return parts.length ? `(${parts.join(` ${t("filterNode.or")} `)})` : "";
} }
if ("not" in node) { if ("not" in node) {

View file

@ -1,11 +1,4 @@
export type FilterType = export type FilterType = "event" | "repo" | "actor" | "action" | "branch" | "keyword" | "field";
| "event"
| "repo"
| "actor"
| "action"
| "branch"
| "keyword"
| "field";
export type FilterOp = export type FilterOp =
| "eq" | "eq"
@ -267,7 +260,15 @@ export const FRAGMENT_PRESETS: FragmentPreset[] = [
}, },
]; ];
export const FILTER_TYPES = ["event", "repo", "actor", "action", "branch", "keyword", "field"] as const; export const FILTER_TYPES = [
"event",
"repo",
"actor",
"action",
"branch",
"keyword",
"field",
] as const;
export const FILTER_OPS: FilterOp[] = [ export const FILTER_OPS: FilterOp[] = [
"eq", "eq",

View file

@ -19,8 +19,8 @@ The console itself is served at `/admin`; its tabs are deep-linkable via the URL
| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) | | `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
| `GET /admin/api/groups/:id/routes` | List a group's routes | | `GET /admin/api/groups/:id/routes` | List a group's routes |
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) | | `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
| `GET /admin/api/groups/:id/fragments` | List a group's named filter fragments | | `GET /admin/api/groups/:id/fragments` | List a group's named filter fragments |
| `PUT /admin/api/groups/:id/fragments` | Replace a group's named filter fragments (owner/admin) | | `PUT /admin/api/groups/:id/fragments` | Replace a group's named filter fragments (owner/admin) |
| `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow | | `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow |
| `GET /admin/api/groups/:id/invites` | List pending invites (owner) | | `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) | | `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
@ -33,7 +33,7 @@ The console itself is served at `/admin`; its tabs are deep-linkable via the URL
| `GET /admin/api/audit` | Audit log (scoped to accessible groups) | | `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
| `GET /admin/api/metrics` | Delivery stats (totals, failure rate, per platform/event/status, recent failures); optional `?groupId=` scope; recent failures scoped to accessible groups for non-super | | `GET /admin/api/metrics` | Delivery stats (totals, failure rate, per platform/event/status, recent failures); optional `?groupId=` scope; recent failures scoped to accessible groups for non-super |
| `GET /admin/api/delivery/:deliveryId` | All send-log attempts for one delivery (group-scoped) | | `GET /admin/api/delivery/:deliveryId` | All send-log attempts for one delivery (group-scoped) |
| `POST /admin/api/test-match` | Stateless filter dry-run — evaluate a filter node against a pasted JSON payload (no event is stored) | | `POST /admin/api/test-match` | Stateless filter dry-run — evaluate a filter node against a pasted JSON payload (no event is stored) |
## Validation ## Validation

View file

@ -66,15 +66,15 @@ All management endpoints (`/admin/api/*`) are documented in the [Admin API](../a
See the [Filter Tutorial](./filters) for a hands-on guide with worked examples. See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
| Type | Matches | Example | | Type | Matches | Example |
| --------- | -------------------- | ---------------------------------- | | --------- | ---------------------------- | ---------------------------------- |
| `event` | GitHub event name | `push`, `pull_*`, `pull_request` | | `event` | GitHub event name | `push`, `pull_*`, `pull_request` |
| `repo` | Repository full name | `org/repo`, `org/*` | | `repo` | Repository full name | `org/repo`, `org/*` |
| `actor` | Sender login | `username`, `[bot]`, `*[bot]` | | `actor` | Sender login | `username`, `[bot]`, `*[bot]` |
| `action` | Event action | `opened`, `closed`, `published` | | `action` | Event action | `opened`, `closed`, `published` |
| `branch` | Branch name | `main`, `feature-?`, `/^release-/` | | `branch` | Branch name | `main`, `feature-?`, `/^release-/` |
| `field` | Any payload field (JSONPath) | `path: "pull_request.user.login"` | | `field` | Any payload field (JSONPath) | `path: "pull_request.user.login"` |
| `keyword` | Text in payload body | `deploy`, `/fix\s+\d+/` | | `keyword` | Text in payload body | `deploy`, `/fix\s+\d+/` |
### Filter Behavior ### Filter Behavior

View file

@ -210,18 +210,18 @@ Matches an arbitrary field of the webhook payload using a dot-separated path, e.
Field filters (and every filter type except `keyword`) accept an `op` to change how the value is compared. The default `eq` keeps the classic glob/regex/exact behaviour. Field filters (and every filter type except `keyword`) accept an `op` to change how the value is compared. The default `eq` keeps the classic glob/regex/exact behaviour.
| Operator | Meaning | | Operator | Meaning |
| -------------- | -------------------------------------------------------------------- | | -------------- | ------------------------------------------------------- |
| `eq` (default) | Equal — globs, regexes and plain text, case-insensitive | | `eq` (default) | Equal — globs, regexes and plain text, case-insensitive |
| `ne` | Not equal (inverse of `eq`) | | `ne` | Not equal (inverse of `eq`) |
| `contains` | Value contains the pattern (substring) | | `contains` | Value contains the pattern (substring) |
| `startsWith` | Value starts with the pattern | | `startsWith` | Value starts with the pattern |
| `endsWith` | Value ends with the pattern | | `endsWith` | Value ends with the pattern |
| `regex` | Explicit regular expression match | | `regex` | Explicit regular expression match |
| `gt` / `gte` | Numeric greater-than / greater-or-equal | | `gt` / `gte` | Numeric greater-than / greater-or-equal |
| `lt` / `lte` | Numeric less-than / less-or-equal | | `lt` / `lte` | Numeric less-than / less-or-equal |
| `in` | Value equals any of the listed patterns | | `in` | Value equals any of the listed patterns |
| `exists` | The field is present (non-null); `match` is ignored | | `exists` | The field is present (non-null); `match` is ignored |
```json ```json
{ "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" } { "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" }
@ -242,10 +242,12 @@ A route can use a nested `ast` to combine filters with explicit grouping instead
"ast": { "ast": {
"all": [ "all": [
{ "type": "event", "match": "pull_request" }, { "type": "event", "match": "pull_request" },
{ "any": [ {
{ "type": "field", "path": "pull_request.user.login", "match": "alice" }, "any": [
{ "type": "field", "path": "pull_request.user.login", "match": "bob" } { "type": "field", "path": "pull_request.user.login", "match": "alice" },
]} { "type": "field", "path": "pull_request.user.login", "match": "bob" }
]
}
] ]
}, },
"targets": [{ "channelId": "..." }] "targets": [{ "channelId": "..." }]

View file

@ -31,12 +31,12 @@ There are **no default routes** — each route must define its own target. If no
Each entry of `targets` is a push destination, so one route can forward to several channels at once (e.g. a Discord channel **and** a Telegram group). `target.platform` selects the platform: `discord` (default) or `telegram`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram**, `target.chatId` (the group/supergroup chat id, e.g. `-1001234567890`) is required and `target.topicId` (the `message_thread_id` of a topic, equivalent of a Discord thread) is optional. There is no fallback to a default channel. Each entry of `targets` is a push destination, so one route can forward to several channels at once (e.g. a Discord channel **and** a Telegram group). `target.platform` selects the platform: `discord` (default) or `telegram`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram**, `target.chatId` (the group/supergroup chat id, e.g. `-1001234567890`) is required and `target.topicId` (the `message_thread_id` of a topic, equivalent of a Discord thread) is optional. There is no fallback to a default channel.
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ---------------- | -------- | -------- | ----------------------------------------------------------------------------------------------- | | ---------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `groupId` | string | Yes | Id of the [group](./groups) this route belongs to | | `groupId` | string | Yes | Id of the [group](./groups) this route belongs to |
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored | | `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event | | `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only | | `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
| `ast` | object | No | Boolean filter tree (`{all:[...]}` / `{any:[...]}` / `{not:{...}}`); takes precedence over `filters` when present | | `ast` | object | No | Boolean filter tree (`{all:[...]}` / `{any:[...]}` / `{not:{...}}`); takes precedence over `filters` when present |
## Discord Role Mentions ## Discord Role Mentions

View file

@ -65,15 +65,15 @@ WebHooker 在 `/admin` 提供内置配置控制台,可在浏览器中管理路
实操指南见[过滤器教程](./filters),包含完整示例。 实操指南见[过滤器教程](./filters),包含完整示例。
| 类型 | 匹配对象 | 示例 | | 类型 | 匹配对象 | 示例 |
| --------- | ---------------- | ---------------------------------- | | --------- | ------------------------ | ---------------------------------- |
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` | | `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
| `repo` | 仓库全名 | `org/repo`, `org/*` | | `repo` | 仓库全名 | `org/repo`, `org/*` |
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` | | `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
| `action` | 事件操作 | `opened`, `closed`, `published` | | `action` | 事件操作 | `opened`, `closed`, `published` |
| `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` | | `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` |
| `field` | 任意载荷字段JSONPath | `path: "pull_request.user.login"` | | `field` | 任意载荷字段JSONPath | `path: "pull_request.user.login"` |
| `keyword` | 载荷正文中的文本 | `deploy`, `/fix\s+\d+/` | | `keyword` | 载荷正文中的文本 | `deploy`, `/fix\s+\d+/` |
### 过滤器行为 ### 过滤器行为

View file

@ -210,18 +210,18 @@
字段过滤器(以及除 `keyword` 之外的所有过滤器类型)可通过 `op` 改变值的比较方式。默认的 `eq` 保持经典的 glob/正则/精确匹配行为。 字段过滤器(以及除 `keyword` 之外的所有过滤器类型)可通过 `op` 改变值的比较方式。默认的 `eq` 保持经典的 glob/正则/精确匹配行为。
| 操作符 | 含义 | | 操作符 | 含义 |
| -------------- | ------------------------------------------ | | ------------ | ---------------------------------------- |
| `eq`(默认) | 相等——通配符、正则与纯文本,不区分大小写 | | `eq`(默认) | 相等——通配符、正则与纯文本,不区分大小写 |
| `ne` | 不相等(`eq` 的反义) | | `ne` | 不相等(`eq` 的反义) |
| `contains` | 值包含模式(子串) | | `contains` | 值包含模式(子串) |
| `startsWith` | 值以模式开头 | | `startsWith` | 值以模式开头 |
| `endsWith` | 值以模式结尾 | | `endsWith` | 值以模式结尾 |
| `regex` | 显式正则表达式匹配 | | `regex` | 显式正则表达式匹配 |
| `gt` / `gte` | 数值大于 / 大于等于 | | `gt` / `gte` | 数值大于 / 大于等于 |
| `lt` / `lte` | 数值小于 / 小于等于 | | `lt` / `lte` | 数值小于 / 小于等于 |
| `in` | 值等于任一列出的模式 | | `in` | 值等于任一列出的模式 |
| `exists` | 字段存在(非 null忽略 `match` | | `exists` | 字段存在(非 null忽略 `match` |
```json ```json
{ "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" } { "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" }
@ -242,10 +242,12 @@
"ast": { "ast": {
"all": [ "all": [
{ "type": "event", "match": "pull_request" }, { "type": "event", "match": "pull_request" },
{ "any": [ {
{ "type": "field", "path": "pull_request.user.login", "match": "alice" }, "any": [
{ "type": "field", "path": "pull_request.user.login", "match": "bob" } { "type": "field", "path": "pull_request.user.login", "match": "alice" },
]} { "type": "field", "path": "pull_request.user.login", "match": "bob" }
]
}
] ]
}, },
"targets": [{ "channelId": "..." }] "targets": [{ "channelId": "..." }]

View file

@ -31,12 +31,12 @@
`targets` 的每一项都是一个推送目标,因此一条路由可同时转发到多个频道(例如一个 Discord 频道**和**一个 Telegram 群组)。`target.platform` 选择平台:`discord`(默认)或 `telegram`。**Discord** 目标要求 `target.channelId`(可选 `target.threadId` 指定子区);**Telegram** 目标要求 `target.chatId`(群组/超级群组 id`-1001234567890`),可选 `target.topicId`(话题的 `message_thread_id`,相当于 Discord 子区)。没有默认频道回退。 `targets` 的每一项都是一个推送目标,因此一条路由可同时转发到多个频道(例如一个 Discord 频道**和**一个 Telegram 群组)。`target.platform` 选择平台:`discord`(默认)或 `telegram`。**Discord** 目标要求 `target.channelId`(可选 `target.threadId` 指定子区);**Telegram** 目标要求 `target.chatId`(群组/超级群组 id`-1001234567890`),可选 `target.topicId`(话题的 `message_thread_id`,相当于 Discord 子区)。没有默认频道回退。
| 字段 | 类型 | 必需 | 说明 | | 字段 | 类型 | 必需 | 说明 |
| ---------------- | -------- | ---- | ------------------------------------------------------------------------ | | ---------------- | -------- | ---- | ------------------------------------------------------------------------------------- |
| `groupId` | string | 是 | 路由所属[分组](./groups)的 id | | `groupId` | string | 是 | 路由所属[分组](./groups)的 id |
| `fallback` | boolean | 否 | 为 `true` 时仅在没有其他非 fallback 路由匹配时才触发;其自身过滤器被忽略 | | `fallback` | boolean | 否 | 为 `true` 时仅在没有其他非 fallback 路由匹配时才触发;其自身过滤器被忽略 |
| `stop` | boolean | 否 | 为 `true` 且该路由匹配时,不再评估后续路由 | | `stop` | boolean | 否 | 为 `true` 且该路由匹配时,不再评估后续路由 |
| `discordRoleIds` | string[] | 否 | 路由触发时要提醒的 Discord 身份组 id仅对 Discord 目标生效 | | `discordRoleIds` | string[] | 否 | 路由触发时要提醒的 Discord 身份组 id仅对 Discord 目标生效 |
| `ast` | object | 否 | 布尔过滤器树(`{all:[...]}` / `{any:[...]}` / `{not:{...}}`);存在时优先于 `filters` | | `ast` | object | 否 | 布尔过滤器树(`{all:[...]}` / `{any:[...]}` / `{not:{...}}`);存在时优先于 `filters` |
## Discord 身份组提醒 ## Discord 身份组提醒

View file

@ -241,7 +241,7 @@ export function evaluateFilterNode(
export function explainFilter(filter: Filter): string { export function explainFilter(filter: Filter): string {
const patterns = toPatterns(filter); const patterns = toPatterns(filter);
const value = patterns.map((m) => JSON.stringify(m)).join(" or "); const value = patterns.map((m) => JSON.stringify(m)).join(" or ");
const label = filter.type === "field" ? filter.path ?? "field" : filter.type; const label = filter.type === "field" ? (filter.path ?? "field") : filter.type;
const op = filter.op ?? "eq"; const op = filter.op ?? "eq";
let base: string; let base: string;
if (filter.type === "keyword") { if (filter.type === "keyword") {

View file

@ -17,9 +17,7 @@ interface D1FragmentRow {
export async function loadFragments(db: D1Database): Promise<NamedFragment[]> { export async function loadFragments(db: D1Database): Promise<NamedFragment[]> {
try { try {
const stmt = db.prepare( const stmt = db.prepare("SELECT id, group_id, name, node FROM d1_fragments ORDER BY id");
"SELECT id, group_id, name, node FROM d1_fragments ORDER BY id",
);
if (typeof stmt.all !== "function") return []; if (typeof stmt.all !== "function") return [];
const { results } = await stmt.all<D1FragmentRow>(); const { results } = await stmt.all<D1FragmentRow>();
if (!results || results.length === 0) return []; if (!results || results.length === 0) return [];
@ -35,10 +33,7 @@ export async function loadFragments(db: D1Database): Promise<NamedFragment[]> {
} }
} }
export async function saveFragments( export async function saveFragments(db: D1Database, fragments: NamedFragment[]): Promise<void> {
db: D1Database,
fragments: NamedFragment[],
): Promise<void> {
const now = Date.now(); const now = Date.now();
const statements: D1PreparedStatement[] = [ const statements: D1PreparedStatement[] = [
db.prepare("DELETE FROM d1_fragments"), db.prepare("DELETE FROM d1_fragments"),

View file

@ -151,14 +151,7 @@ export interface Group {
logTarget?: RouteTarget; logTarget?: RouteTarget;
} }
export type FilterType = export type FilterType = "event" | "repo" | "actor" | "action" | "branch" | "keyword" | "field";
| "event"
| "repo"
| "actor"
| "action"
| "branch"
| "keyword"
| "field";
export type FilterOp = export type FilterOp =
| "eq" | "eq"

View file

@ -37,8 +37,29 @@ import { getTenantSecret, setTenantSecret, deleteTenantSecret } from "./tenants"
import { cfEnv } from "../cf"; import { cfEnv } from "../cf";
import { log } from "../lib/log"; import { log } from "../lib/log";
const VALID_FILTER_TYPES = new Set(["event", "repo", "actor", "action", "branch", "keyword", "field"]); const VALID_FILTER_TYPES = new Set([
const VALID_OPS = new Set(["eq", "ne", "contains", "startsWith", "endsWith", "regex", "gt", "gte", "lt", "lte", "in", "exists"]); "event",
"repo",
"actor",
"action",
"branch",
"keyword",
"field",
]);
const VALID_OPS = new Set([
"eq",
"ne",
"contains",
"startsWith",
"endsWith",
"regex",
"gt",
"gte",
"lt",
"lte",
"in",
"exists",
]);
const ID_RE = /^[a-z0-9][a-z0-9-]*$/; const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
const HOST_RE = const HOST_RE =
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/; /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
@ -1175,9 +1196,7 @@ export async function adminGroupFragmentsPut(
} }
/** POST /admin/api/test-match */ /** POST /admin/api/test-match */
export async function adminApiTestMatch( export async function adminApiTestMatch(event: H3Event): Promise<Record<string, unknown>> {
event: H3Event,
): Promise<Record<string, unknown>> {
await requireAnyAccess(event); await requireAnyAccess(event);
const body = await readJsonBody(event); const body = await readJsonBody(event);
if (!body) return respondError(event, 400, "Invalid JSON body"); if (!body) return respondError(event, 400, "Invalid JSON body");