Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/botnadzor/extension/llms.txt

Use this file to discover all available pages before exploring further.

Botnadzor Extension uses Zod for runtime type validation and TypeScript type inference. All models are defined in src/shared/@model/ and use zod/mini for smaller bundle size.

Import Convention

import { z } from "zod/mini";

// Define schema
export const mySchema = z.readonly(z.object({ ... }));

// Infer TypeScript type
export type MyType = z.infer<typeof mySchema>;
Note: Use z.exactOptional() instead of z.optional() for cleaner types.

RootConfig

Central configuration fetched from the static API that defines extension version ranges and remote system URLs. Schema: src/shared/@model/root-config.ts:16-53
type RootConfig = {
  extensionVersionRange: SemverRange;  // e.g., ">=2.0.0 <3.0.0"
  generatedAt: IsoDateTime;
  remoteSystemLookup: {
    dynamicApi: {
      aliasLookup: Record<string, { role?: "primary" }>;
    };
    frontend: {
      aliasLookup: Record<string, { role?: "primary" }>;
    };
    staticApi: {
      aliasLookup: Record<string, { role?: "primary" }>;
      listLookup: Record<StaticListId, {
        generatedAt: IsoDateTime;
        itemCount: number;
      }>;
    };
  };
};
Usage:
const rootConfig = await rootConfigService.get();
const isSupported = semverSatisfies(
  currentVersion,
  rootConfig.extensionVersionRange
);
Seed: The extension includes a seed config (root-config/seed.json) that’s used on first launch before fetching from the server.

UserConfig

User preferences stored in sync storage. Schema: src/shared/@model/user-config.ts:14-20
type UserConfig = {
  tagOverrideLookup: Record<string, {
    colorForHighlight?: HexColor;  // Override tag color
    hidden?: true;                 // Hide this tag's accounts
  }>;
  fansDisplay: "default" | "table";  // How to display reaction lists
  collectingComments?: true;         // Opt-in to comment collection
};
Default:
const defaultUserConfig: UserConfig = {
  tagOverrideLookup: {},
  fansDisplay: "default"
};
Usage:
const userConfig = await userConfigService.get();
if (userConfig.collectingComments) {
  await collectingService.collectCommentIfNeeded(comment);
}

Auth Models

Authentication state management types. Schema: src/shared/@model/auth.ts

PermissionLookup

type PermissionLookup = {
  getRegDate?: true;      // Can fetch registration dates
  inspectAccount?: true;  // Can inspect accounts
  reportAccount?: true;   // Can report accounts
};

AuthStatus

type AuthStatus =
  | {
      state: "empty";  // No access code entered
      accessCode: string;
      accessCodeEnteredAt: IsoDateTime;
    }
  | {
      state: "invalid";  // Access code rejected
      accessCode: string;
      accessCodeEnteredAt: IsoDateTime;
      accessCodeRecognized: boolean;  // true if code exists but expired
      errorMessage: string;
    }
  | {
      state: "valid";  // Successfully authenticated
      expiresAt?: IsoDateTime;
      accessLevel: number;
      pointCount: number;
      permissionLookup: PermissionLookup;
    }
  | {
      state: "unknown";  // API unavailable, can't verify
    };

AuthCheck

type AuthCheck =
  | { state: "idle"; lastFinishedAt?: IsoDateTime }
  | { state: "ongoing"; startedAt: IsoDateTime };
Usage:
const authStatus = authService.getAuthStatus();
if (authStatus.state === "valid" && authStatus.permissionLookup.inspectAccount) {
  // User can inspect accounts
}

StaticLists

Data structures for the five static lists: accounts, tags, walls, announcements, and insertions. Schema: src/shared/@model/static-lists.ts

StaticListId

type StaticListId = "accounts" | "tags" | "walls" | "announcements" | "insertions";

Static List Items

AccountListItem

type AccountListItem = {
  vkId?: number;         // VK ID (if known)
  vkNickname?: string;   // VK nickname (if known)
  tagIds: string[];      // Associated tag IDs
};

TagListItem

type TagListItem = {
  id: string;                     // Unique tag ID
  name: string;                   // Display name (e.g., "Бот")
  description?: string;           // Full description
  color?: HexColor;               // Badge color
  colorForHighlight?: HexColor;   // Highlight color
  botnadzorPage?: true;           // Has Botnadzor page link
  botnadzorCard?: true;           // Has Botnadzor card link
};

WallListItem

type WallListItem = {
  vkId: number;       // Wall/community ID
  skip?: true;        // Skip comment collection for this wall
};

AnnouncementListItem

type AnnouncementListItem = {
  createdAt: IsoDateTime;
  extensionVersionRange: SemverRange;
  extensionVersionRangeForToast?: SemverRange;  // Narrower range for toasts
  text: string;       // Markdown content
  title: string;
  updatedAt: IsoDateTime;
};

InsertionListItem

type InsertionListItem = InsertionConfig & {
  id: string;
  extensionVersionRange?: SemverRange;  // Optional version constraint
};

StaticListSummary

Each list has a summary type that provides aggregate statistics:
// Example for accounts list
type AccountsListSummary = {
  itemCount: number;
  vkIdCount: number;
  vkNicknameCount: number;
};

// Example for tags list
type TagsListSummary = {
  itemCount: number;
};
Usage:
const account = await staticListsService.findItem(
  "accounts",
  "vkId",
  123456
);

const tags = await Promise.all(
  account.tagIds.map(tagId => 
    staticListsService.findItem("tags", "id", tagId)
  )
);

InsertionConfigs

Configuration schemas for each insertion variant. Base Schema: src/shared/@model/insertion-configs/shared/schema.ts

Common Types

type StringDataSelector = 
  | { selector: string; attribute: string }
  | { selector: string; textContent: true }
  | { fiberKey: string }  // React fiber key
  | false;  // Disabled

type ElementSelector = 
  | { selector: string }
  | false;

type UiPlacement =
  | { target: string; position: "beforebegin" | "afterend" | "beforeend" }
  | { target: string }  // For wrapping
  | false;  // Disabled

type MarkupEdit = 
  | { type: "hide"; target: string }
  | { type: "show"; target: string };

AccountInsertionConfig

Schema: src/shared/@model/insertion-configs/account.ts
type AccountInsertionConfig = {
  variant: "account";
  observeSelector: string;
  markup: {
    data: {
      accountAvatar: ElementSelector | StringDataSelector;
      accountIdentifier: StringDataSelector;
      accountName: StringDataSelector;
    };
    ui: {
      actionBar: UiPlacement;
      affiliationBadge: UiPlacement;
      affiliationHighlight: UiPlacement;
      regDate: UiPlacement;
    };
    edits: MarkupEdit[];
  };
};

CommentInsertionConfig

Schema: src/shared/@model/insertion-configs/comment.ts
type CommentInsertionConfig = {
  variant: "comment";
  observeSelector: string;
  markup: {
    data: {
      accountAvatar: ElementSelector | StringDataSelector;
      accountIdentifier: StringDataSelector;
      accountName: StringDataSelector;
      commentIdentifier: StringDataSelector | false;
      postCommentCount: StringDataSelector | false;
    };
    ui: {
      actionBar: UiPlacement;
      affiliationBadge: UiPlacement;
      affiliationHighlight: UiPlacement;
      regDate: UiPlacement;
    };
    edits: MarkupEdit[];
  };
};

ReplyFormInsertionConfig

Schema: src/shared/@model/insertion-configs/reply-form.ts
type ReplyFormInsertionConfig = {
  variant: "replyForm";
  observeSelector: string;
  markup: {
    data: {
      accountIdentifier: StringDataSelector;
    };
    ui: {
      bnCardAttachmentButton: UiPlacement;
    };
    edits: MarkupEdit[];
  };
};

ReviewInsertionConfig

Schema: src/shared/@model/insertion-configs/review.ts
type ReviewInsertionConfig = {
  variant: "review";
  observeSelector: string;
  markup: {
    data: {
      accountAvatar: ElementSelector | StringDataSelector;
      accountIdentifier: StringDataSelector;
      accountName: StringDataSelector;
      reviewIdentifier: StringDataSelector | false;
    };
    ui: {
      actionBar: UiPlacement;
      affiliationBadge: UiPlacement;
      affiliationHighlight: UiPlacement;
      regDate: UiPlacement;
    };
    edits: MarkupEdit[];
  };
};
Union Type:
type InsertionConfig = 
  | AccountInsertionConfig
  | CommentInsertionConfig
  | ReplyFormInsertionConfig
  | ReviewInsertionConfig;

type InsertionVariant = InsertionConfig["variant"];

AccountAffiliation

Represents an account’s affiliation with tags (bot, spam, etc.). Schema: src/shared/@model/account-affiliation.ts:4-14
type AccountAffiliation = {
  color: HexColor;                    // Primary color from first tag
  colorForHighlight: HexColor;        // Highlight color
  tags: [TagListItem, ...TagListItem[]];  // Non-empty array of tags
  hidden?: boolean;                   // User has hidden this tag
  botnadzorPage?: true;               // Has Botnadzor page link
  botnadzorCard?: true;               // Has Botnadzor card link
};
Fallback Color:
const fallbackHexColor: HexColor = "#888888";
Usage:
const affiliation = await affiliationService.checkAccount("id123");
if (affiliation && !affiliation.hidden) {
  // Show colored badge with first tag
  showBadge(affiliation.tags[0].name, affiliation.color);
}

Inspector Models

Data structures for the account inspector feature. Schema: src/shared/@model/inspector.ts

InspectorAccountInfo

type InspectorAccountInfo = {
  vkDomain: VkDomain;   // e.g., "id123" or "nickname"
  name: string;         // Display name
  avatarUrl: string;    // Avatar URL
};

InspectorTrigger

type InspectorTrigger =
  | {
      type: "comment";
      postType: "photo" | "video" | "wall";
      wallVkId: VkId;
      postVkId: VkId;
      commentVkId: VkId;
    }
  | {
      type: "review";
      wallVkId: VkId;
      reviewVkId: PositiveVkId;
    };

InspectorInstanceConfig

type InspectorInstanceConfig = {
  accountInfo: InspectorAccountInfo;
  trigger: InspectorTrigger;
  tab: "activity" | "report";
  triggeredAt: IsoDateTime;
};
Report Constraints:
const reportTextMinLength = 10;
const reportTextMaxLength = 200;
Usage:
await inspectorService.trigger(contentId, {
  accountInfo: {
    vkDomain: "id123",
    name: "John Doe",
    avatarUrl: "https://..."
  },
  trigger: {
    type: "comment",
    postType: "wall",
    wallVkId: -123,
    postVkId: 456,
    commentVkId: 789
  }
});

Additional Primitives

VK Types

Schema: src/shared/@primitives/vk.ts
type VkId = number;            // Can be negative for communities
type PositiveVkId = number;    // Must be > 0 (users only)
type VkDomain = string;        // e.g., "id123" or "nickname"
type VkNickname = string;      // Alphanumeric + underscore

type AccountIdentifier = 
  | { kind: "vkId"; value: PositiveVkId }
  | { kind: "vkNickname"; value: VkNickname };

Temporal Types

Schema: src/shared/@primitives/temporal.ts
type IsoDate = string;         // YYYY-MM-DD
type IsoDateTime = string;     // ISO 8601 with timezone

Misc Types

Schema: src/shared/@primitives/misc.ts
type HexColor = string;        // #RRGGBB format
type ContentId = string;       // Unique ID for content instances
type TagId = string;           // Tag identifier
type TagSuggestion = string;   // Tag suggestion for reports

Model Best Practices

Schema Design

  1. Always use readonly: Wrap schemas with z.readonly() for immutability
  2. Use exactOptional: Prefer z.exactOptional() over z.optional()
  3. Validate enums: Use z.enum() or z.literal() for string constants
  4. Document types: Export both schema and inferred type

Type Safety

// Good: Explicit type inference
export const mySchema = z.readonly(z.object({ ... }));
export type MyType = z.infer<typeof mySchema>;

// Good: Discriminated unions
export const statusSchema = z.discriminatedUnion("state", [
  z.object({ state: z.literal("idle") }),
  z.object({ state: z.literal("loading") })
]);

Validation

// Parse with error handling
const result = mySchema.safeParse(data);
if (!result.success) {
  logger.error("Invalid data: {error}", { error: result.error });
  return;
}
const validData = result.data;

Schema Evolution

When updating schemas:
  1. Use z.exactOptional() for new fields
  2. Provide sensible defaults
  3. Test with old data to ensure backwards compatibility
  4. Version schemas when breaking changes are needed
// Adding a new optional field
export const mySchemaV2 = z.readonly(
  z.extend(mySchemaV1, {
    newField: z.exactOptional(z.string())
  })
);

Build docs developers (and LLMs) love