Skip to main content

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

FileResponsibility
ExtensionBaseService.tsBaseService subclass with org-scoping and extensionId audit tagging
extensionEventBus.tsScoped event bus factory — auto-tags events, tracks subscriptions
extensionWidgets.tsWidget registration/unregistration tied to extension lifecycle
extensionCommands.tsCommand palette action registration/unregistration
version.tsCORE_API_VERSION, semver compatibility checker
types.tsShared TypeScript types re-exported for extension authors
index.tsBarrel 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 enforcementorganizationId is required on all write operations; unscoped writes throw a ValidationError.
  • Extension event bus integrationthis.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
}

extensionWidgets

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.

ExtensionWidgetContribution

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

CallerUses
All 12 extension modules (src/extensions/*/)The entire public API surface
src/shared/platform/ExtensionActivationService.tsisCompatible(), unregisterExtensionWidgets(), unregisterExtensionActions()
src/features/extensions/Reads getRegisteredWidgets(), getRegisteredActions() for the Marketplace UI