Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
shared/extension-api — API Reference
The shared/extension-api module is the contract boundary between the GrantMaster core and its extension system. Extensions import only from this module — never from src/core/, src/features/, or other internal paths. It provides a versioned, stable API surface for data access, event bus interaction, widget registration, and command palette integration.
Module Map
| File | Responsibility |
|---|
ExtensionBaseService.ts | BaseService subclass with org-scoping and extensionId audit tagging |
extensionEventBus.ts | Scoped event bus factory — auto-tags events, tracks subscriptions |
extensionWidgets.ts | Widget registration/unregistration tied to extension lifecycle |
extensionCommands.ts | Command palette action registration/unregistration |
version.ts | CORE_API_VERSION, semver compatibility checker |
types.ts | Shared TypeScript types re-exported for extension authors |
index.ts | Barrel export — the only import point for extension code |
ExtensionBaseService
Extensions extend ExtensionBaseService<T> instead of BaseService<T> directly.
import { ExtensionBaseService } from '@/shared/extension-api';
export class CalendarEventService extends ExtensionBaseService<CalendarEvent> {
protected collectionName = 'grantCalendarEvents';
protected serviceName = 'CalendarEventService';
protected extensionId = 'grant-calendar'; // required — used in audit logs + events
}
What ExtensionBaseService adds over BaseService
extensionId tagging — every audit log entry written by the service automatically includes { extensionId } in its metadata.
- Org-scoping enforcement —
organizationId is required on all write operations; unscoped writes throw a ValidationError.
- Extension event bus integration —
this.eventBus is automatically the scoped bus created by createExtensionEventBus(extensionId).
Everything else (Zod validation, quota checks, retry logic, ServiceResult wrapping) is inherited from BaseService.
extensionEventBus
Creates a scoped wrapper around the global EventBus that tracks subscriptions per extension for lifecycle-safe cleanup.
createExtensionEventBus()
import { createExtensionEventBus } from '@/shared/extension-api';
const bus = createExtensionEventBus('grant-calendar');
// Subscribe — tracked internally, cleaned up on extension deactivation
const unsubscribe = bus.on(SystemEventType.GRANT_WON, (event) => {
// handle event
});
// Emit — auto-tagged with { extensionId: 'grant-calendar' } in event.metadata
await bus.emit({
type: SystemEventType.GRANT_CALENDAR_DEADLINE_REMINDER,
organizationId: 'org-123',
userId: 'user-456',
severity: EventSeverity.INFO,
timestamp: new Date(),
payload: { grantId: 'g1', daysUntilDeadline: 7 },
});
// Canonical status projections
const projection = await bus.getStatusProjection(entityType, entityId);
const freshness = bus.getFreshnessForConsumer(consumer);
ExtensionEventBus Interface
interface ExtensionEventBus {
on<T>(eventType: SystemEventType, handler: EventHandler<T>): () => void;
emit<T>(event: Omit<SystemEvent<T>, 'id'>): Promise<void>;
getStatusProjection(entityType, entityId): Promise<CanonicalStatusProjection | null>;
getFreshnessForConsumer(consumer: CanonicalConsumer): CanonicalProjectionFreshness;
unsubscribeAll(): void; // called automatically on extension deactivation
}
Registers and unregisters dashboard widgets tied to an extension’s lifecycle.
import {
registerExtensionWidgets,
unregisterExtensionWidgets,
getRegisteredWidgets,
} from '@/shared/extension-api';
// On extension activation:
await registerExtensionWidgets('grant-calendar', manifest.contributions.widgets);
// On extension deactivation:
await unregisterExtensionWidgets('grant-calendar');
Internally delegates to WidgetRegistryService in shared/platform. Widget definitions are persisted to Firestore so useWidgets() dashboard hooks can discover them; deactivation removes both definitions and user assignments.
interface ExtensionWidgetContribution {
id: string; // e.g. 'deadline-calendar'
name: string;
description: string;
component: string; // component identifier for lazy loading
defaultSize: 'sm' | 'md' | 'lg' | 'xl';
allowedRoles?: string[];
defaultEnabled: boolean;
}
extensionCommands
Registers and unregisters ⌘K command palette actions for an extension.
import {
registerExtensionActions,
unregisterExtensionActions,
getRegisteredActions,
} from '@/shared/extension-api';
// On activation:
registerExtensionActions('grant-calendar', manifest.contributions.commands);
// On deactivation:
unregisterExtensionActions('grant-calendar');
Each ExtensionCommandContribution is converted to a full CommandAction with a qualified ID (ext_{extensionId}_{commandId}) and registered via ActionRegistry from features/command-palette.
ExtensionCommandContribution
interface ExtensionCommandContribution {
id: string;
label: string;
description?: string;
icon?: string; // lucide-react icon name, resolved at render time
keywords?: string[];
requiredPermissions?: Permission[];
handler: () => void | Promise<void>;
}
version.ts
Tracks the version of the core API exposed to extensions and provides a compatibility checker used at extension activation time.
import { CORE_API_VERSION, isCompatible, parseSemver } from '@/shared/extension-api';
console.log(CORE_API_VERSION); // "1.0.0"
// Check if this core satisfies a manifest's declared range
isCompatible('^1.0.0'); // true
isCompatible('~1.0.0'); // true (same major.minor)
isCompatible('2.0.0'); // false (major mismatch)
Supported range operators: exact (1.0.0), caret (^1.0.0 — same major), tilde (~1.0.0 — same major.minor).
Extension manifests declare compatibility.coreApiVersion: "^1.0.0". ExtensionActivationService calls isCompatible() before installing any extension and rejects incompatible ones with a clear error.
Extension Development Quick-Start
// my-extension/services/MyService.ts
import { ExtensionBaseService } from '@/shared/extension-api';
import type { MyEntity } from '../types';
export class MyEntityService extends ExtensionBaseService<MyEntity> {
protected collectionName = 'myExtensionEntities';
protected serviceName = 'MyEntityService';
protected extensionId = 'my-extension-id'; // matches manifest.id
}
// my-extension/index.ts — activation hook
import { createExtensionEventBus, registerExtensionWidgets, registerExtensionActions } from '@/shared/extension-api';
import manifest from './manifest';
export async function activate() {
const bus = createExtensionEventBus(manifest.id);
bus.on(SystemEventType.GRANT_WON, handleGrantWon);
await registerExtensionWidgets(manifest.id, manifest.contributions.widgets);
registerExtensionActions(manifest.id, manifest.contributions.commands);
}
export async function deactivate() {
await unregisterExtensionWidgets(manifest.id);
unregisterExtensionActions(manifest.id);
// bus.unsubscribeAll() is called automatically by ExtensionActivationService
}
Callers
| Caller | Uses |
|---|
All 12 extension modules (src/extensions/*/) | The entire public API surface |
src/shared/platform/ExtensionActivationService.ts | isCompatible(), unregisterExtensionWidgets(), unregisterExtensionActions() |
src/features/extensions/ | Reads getRegisteredWidgets(), getRegisteredActions() for the Marketplace UI |