Providers bridge the gap between deterministic Git hooks and your agile ecosystem. Use them to fetch rich ticket status from Jira, Linear, or custom databases without inflating the core
defense-in-depthengine.
If the default file provider does not meet your needs, you can easily wire your own TicketStateProvider.
sequenceDiagram
participant Hook as Git Hook Engine
participant CP as Custom Provider
participant API as External Service (Linear/Jira)
Hook->>CP: resolve("TK-123")
activate CP
CP->>API: GET /api/tickets/TK-123
API-->>CP: JSON Response
CP-->>Hook: TicketRef { id, phase, type }
deactivate CP
Note right of Hook: Pipeline passes context to Guards
All providers must implement the TicketStateProvider interface:
export interface TicketStateProvider {
/**
* Name of the provider. Used in `defense.config.yml`.
*/
name: string;
/**
* Resolves context about a ticket given its ID.
*
* @param ticketId - The ID of the ticket to look up.
* @returns A promise resolving to a TicketRef or undefined if not found/error.
*/
resolve(ticketId: string): Promise<TicketRef | undefined>;
}Here is an example custom provider that fetches ticket identities from an external JSON API:
import type { TicketStateProvider, TicketRef, ProviderConfig } from "defense-in-depth";
export class ApiTicketProvider implements TicketStateProvider {
readonly name = "myApi";
private endpoints: string;
constructor(config?: ProviderConfig) {
this.endpoints = config?.providerConfig?.endpoint ?? "https://api.mycompany.com/tickets";
}
async resolve(ticketId: string): Promise<TicketRef | undefined> {
// [Anti-Spam Guard] In production, you should check a local FileSystem cache
// (e.g. .git/defense-cache.json) before hitting the network to keep Git hooks < 50ms.
// [Dangling Socket Guard] Always use an AbortController so Promise.race timeouts
// don't leave zombie connections hanging in the background.
const controller = new AbortController();
const timeoutMsg = setTimeout(() => controller.abort(), 1000);
try {
const response = await fetch(`${this.endpoints}/${ticketId}`, {
signal: controller.signal
});
clearTimeout(timeoutMsg);
if (!response.ok) return undefined;
const data = await response.json();
return {
id: data.id,
phase: data.status,
type: data.team === 'docs' ? 'docs' : 'feat'
};
} catch (err) {
clearTimeout(timeoutMsg);
// Providers must NOT throw exceptions! They should gracefully absorb errors.
if (err.name === 'AbortError') {
console.warn(`\n[myApi] Timeout fetching ${ticketId}. Proceeding blindly.`);
}
return undefined;
}
}
}When developing custom providers, you must adhere to the Provider Contract:
- Graceful Failures: I/O is inherently unsafe. Do not throw exceptions, as it will crash the git hook.
catchall asynchronous errors and returnundefinedwith a simple warning. - Speed & Timeout: Since hooks block developers from committing/pushing, providers must be fast.
defense-in-depthwrappers invoke your hook with a default global timeout. - No Side-Effects: Your
resolvefunction should only read data, not modify external systems or file states.
Note: In v0.3.x, dynamically loaded custom providers are still experimental. Built-in providers (like the file provider) ship with the engine. Future versions will support defense-in-depth scanning for custom .js provider classes based on configuration.