Deep extension system implementation knowledge: IExtensionLoader interface contracts, IndexedDB storage schema, version-compare cache strategy, contribution point registration, extension sandbox, and the .foxe packaging format.
55
61%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.github/skills/extensions-internals/SKILL.mdDefined in packages/suite-base/src/services/extension/IExtensionLoader.ts:
export type TypeExtensionLoader = "browser" | "server" | "filesystem";
export type LoadedExtension = {
buffer?: Uint8Array;
raw: string;
};
export type InstallExtensionProps = {
foxeFileData: Uint8Array;
file?: File;
externalId?: string;
};
export interface IExtensionLoader {
readonly namespace: Namespace; // "org" | "local"
readonly type: TypeExtensionLoader; // "browser" | "server" | "filesystem"
getExtension(id: string): Promise<ExtensionInfo | undefined>;
getExtensions(): Promise<ExtensionInfo[]>;
loadExtension(id: string): Promise<LoadedExtension>;
installExtension(data: InstallExtensionProps): Promise<ExtensionInfo>;
uninstallExtension(id: string): Promise<void>;
}⚠️
installExtensiontakes anInstallExtensionPropsobject (withfoxeFileData), not a URL string. The type union is"browser" | "server" | "filesystem"— there is no"indexeddb"or"remote"value.
Implementations:
| Class | File | namespace | type |
|---|---|---|---|
IdbExtensionLoader | services/extension/IdbExtensionLoader.ts | constructor arg | "browser" |
RemoteExtensionLoader | services/extension/RemoteExtensionLoader.ts | constructor arg | "server" |
DesktopExtensionLoader | suite-desktop/src/renderer/services/DesktopExtensionLoader.ts | "local" | "filesystem" |
All loaders implement this interface — the catalog provider doesn't know the backing store.
IdbExtensionLoader delegates persistence to IdbExtensionStorage
(packages/suite-base/src/services/extension/IdbExtensionStorage.ts).
`${KEY_WORKSPACE_PREFIX}lichtblick-extensions-${namespace}` (one DB per namespace)1metadata — keyPath "id", value ExtensionInfoextensions — keyPath "info.id", value StoredExtension1. Receive InstallExtensionProps { foxeFileData, file?, externalId? }
2. Decompress + extract package.json / dist entry from the .foxe
3. validatePackageInfo() builds ExtensionInfo
4. Persist ExtensionInfo to `metadata` + StoredExtension to `extensions`1. storage.get(id) → StoredExtension (throws "Extension not found" if missing)
2. decompressFile(content) → extractFoxeFileContent(ALLOWED_FILES.EXTENSION)
3. Return LoadedExtension { buffer?, raw }"org" extensions: managed by organization, auto-synced"local" extensions: user-installed, never auto-removed-{namespace})packages/suite-desktop/src/renderer/services/DesktopExtensionLoader.ts \u2014 namespace = "local",
type = "filesystem". Reads installed extensions from the desktop file system (via the preload
bridge) rather than IndexedDB. Same IExtensionLoader contract.
packages/suite-base/src/services/extension/RemoteExtensionLoader.ts \u2014 type = "server" (the
remote/org loader). The type value is "server", not "remote" \u2014 the only valid type
values are "browser", "server", and "filesystem".
├── package.json (required)
├── dist/
│ └── index.js (required — bundled extension entry)
├── README.md (optional)
└── CHANGELOG.md (optional){
"name": "@publisher/extension-name",
"version": "1.2.3",
"displayName": "Human Readable Name",
"description": "What this extension does",
"publisher": "publisher-name",
"main": "dist/index.js"
}The
package.jsoncarries metadata only. Contributions (panels, converters, aliases, camera models) are registered at runtime inactivate(ctx)\u2014 see "Contribution Registration" below.
⚠️ Contributions are not declared statically in
package.json. There is nolichtblick.panels/messageConverterscontributions key. Instead the extension's bundled source is executed and registers contributions at runtime via theactivate(ctx)callback.
buildContributionPoints.ts
(packages/suite-base/src/providers/helpers/buildContributionPoints.ts) executes the extension
source with new Function("module", "require", source), then calls
module.exports.activate(ctx). The ExtensionContext (ctx) exposes:
const ctx: ExtensionContext = {
mode, // "production" | "test" | "development"
registerPanel(registration), // panelId = `${qualifiedName}.${registration.name}`
registerMessageConverter(args), // collects InstalledMessageConverter + panelSettings
registerTopicAliases(aliasFn), // note: registerTopicAliases (plural), not ...Function
registerCameraModel({ name, modelBuilder }),
};buildContributionPoints returns the accumulated
{ panels, messageConverters, topicAliasFunctions, panelSettings, cameraModels }.
interface ExtensionCatalogState {
installedExtensions: ExtensionInfo[];
installedPanels: Map<string, RegisteredPanel>;
installedMessageConverters: MessageConverter[];
installedTopicAliasFunctions: TopicAliasFunction[];
installedCameraModels: CameraModel[];
// Actions
refreshExtensions: () => Promise<void>;
installExtension: (loader: IExtensionLoader, url: string) => Promise<void>;
uninstallExtension: (loader: IExtensionLoader, id: string) => Promise<void>;
}refreshExtensions() called
→ For each loader: getExtensions()
→ For each extension: loadExtension()
→ Execute extension code in sandbox
→ Extension calls activate(context)
→ context.registerPanel() / context.registerMessageConverter()
→ Zustand state updated with new contributionsExtensions run in a restricted context built by buildContributionPoints.ts:
new Function("module", "require", source) — require only resolves
react and react-domExtensionContext API surface:
interface ExtensionContext {
mode: "production" | "test" | "development";
registerPanel(registration: ExtensionPanelRegistration): void;
registerMessageConverter<Src>(args: RegisterMessageConverterArgs<Src>): void;
registerTopicAliases(aliasFunction: TopicAliasFunction): void;
registerCameraModel(args: RegisterCameraModelArgs): void;
}// Inside extension's activate():
export function activate(context: ExtensionContext) {
context.registerPanel({
name: "MyPanel",
initPanel: (panelAPI: PanelExtensionContext) => {
panelAPI.onRender = (renderState, done) => {
// Render panel content
done();
};
panelAPI.subscribe([{ topic: "/my_topic" }]);
},
});
}Priority order when same contribution exists in multiple sources:
main field, incorrect contribution format, version string format0900ce3
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.