Plugin System
What is a plugin?
The LearnCard Plugin System is the modular foundation that enables extensibility of the LearnCard Core through encapsulated units of functionality. This document explains how plugins are structured, loaded, and used within the LearnCard ecosystem.
Plugins Are:
Core bundles of execution.
Have no hard requirements they must conform to.
Atomic.
Not categorical.
Typically fall into larger execution workflows. See Control Planes.
Plugins can implement arbitrary functionality, or optionally choose to implement any number of Control Planes. Via a common instantiation pattern, it is also easily possible for plugins to depend on other plugins via their exposed interfaces, as well as to happily hold private and public fields.
The LearnCard Core is designed with minimal base functionality, which is extended through plugins that provide specific capabilities. The plugin system enables developers to include only the features they need, resulting in more lightweight and focused implementations.
Additionally, because plugins and plugin hierarchies work entirely through interfaces rather than explicit dependencies, any plugin can be easily hotswapped by another plugin that implements the same interface, and plugins implementing Control Planes can easily be stacked on top of each other to further enhance the functionality of that Control Plane for the wallet.
Plugin Interface
A LearnCard plugin follows a standard interface structure which includes:
name: A unique identifier string
displayName: A human-readable name for UI display
description: A description of the plugin's functionality
methods: An object containing the functions provided by the plugin
The type system uses generics to track which plugins are added to a LearnCard instance, ensuring type safety when accessing plugin methods.
Adding Plugins
In order to create a fully functioning LearnCard, you will need to add some plugins. Specifically, you will (probably) want to add at least one plugin that implements each Control Plane.
In code, constructing a LearnCard completely from scratch looks like this:
const baseLearnCard = await initLearnCard({ custom: true });
const didkitLearnCard = await baseLearnCard.addPlugin(await getDidKitPlugin());
const didkeyLearnCard = await didkitLearnCard.addPlugin(await getDidKeyPlugin(didkitLearnCard, 'a', 'key'));
// repeat for any more plugins you'd like to add
However, you don't have to start from scratch! Each instantiation function is completely able to add bespoke plugins to it:
const emptyLearnCard = await initLearnCard();
const customLearnCard = await emptyLearnCard.addPlugin(CustomPlugin);
import { LearnCard, Plugin } from 'types/wallet';
export type GetPlugins<LC extends LearnCard<any, any, any>> = LC['plugins'];
export type AddPlugin<LC extends LearnCard<any, any, any>, P extends Plugin> = LearnCard<
[...GetPlugins<LC>, P]
>;
Example: Expiration Plugin
Let's examine how the Expiration Plugin is implemented to understand a typical plugin structure:
The Expiration Plugin extends the credential verification process to check expiration dates:
It depends on the VC Plugin's
verifyCredential
methodIt calls the original method to perform base verification
It adds additional checks for credential expiration
It returns an enhanced verification result
This demonstrates how plugins can build upon each other's functionality in a composable manner.
export const expirationPlugin = (
learnCard: LearnCard<any, any, VerifyExtension>
): ExpirationPlugin => ({
name: 'Expiration',
displayName: 'Expiration Extension',
description: "Adds a check to make sure credentials aren't expired when verifying them",
methods: {
verifyCredential: async (_learnCard, credential, options) => {
const verificationCheck = await learnCard.invoke.verifyCredential(credential, options);
if (credential.expirationDate && new Date() > new Date(credential.expirationDate)) {
verificationCheck.errors.push('expiration error: Credential is expired');
} else if (credential.validFrom && new Date() < new Date(credential.validFrom)) {
verificationCheck.errors.push('expiration error: Credential is not valid yet');
} else if (credential.validUntil && new Date() > new Date(credential.validUntil)) {
verificationCheck.errors.push('expiration error: Credential is no longer valid');
} else {
verificationCheck.checks.push('expiration');
}
return verificationCheck;
},
},
});
Interacting with Plugins
After initialization, applications interact with plugins through the LearnCard instance's invoke
property. The method calls follow this pattern:
This unified interface allows applications to access all plugin functionality through a single entry point, abstracting away the details of which plugin provides which method.
Plugin Data Types
Plugins often work with specific data types, particularly for credential operations. The core types used across plugins include:
UnsignedVC
Unsigned Verifiable Credential data structure
VC
Signed Verifiable Credential with proof
UnsignedVP
Unsigned Verifiable Presentation
VP
Signed Verifiable Presentation with proof
Proof
Cryptographic proof structure
Profile
Entity profile information
These types are defined in the @learncard/types
package and used consistently across all plugins to ensure interoperability.
export const UnsignedVCValidator = z
.object({
'@context': ContextValidator,
id: z.string().optional(),
type: z.string().array().nonempty(),
issuer: ProfileValidator,
credentialSubject: CredentialSubjectValidator.or(CredentialSubjectValidator.array()),
refreshService: RefreshServiceValidator.or(RefreshServiceValidator.array()).optional(),
credentialSchema: CredentialSchemaValidator.or(
CredentialSchemaValidator.array()
).optional(),
// V1
issuanceDate: z.string().optional(),
expirationDate: z.string().optional(),
credentialStatus: CredentialStatusValidator.or(
CredentialStatusValidator.array()
).optional(),
// V2
name: z.string().optional(),
description: z.string().optional(),
validFrom: z.string().optional(),
validUntil: z.string().optional(),
status: CredentialStatusValidator.or(CredentialStatusValidator.array()).optional(),
termsOfUse: TermsOfUseValidator.or(TermsOfUseValidator.array()).optional(),
evidence: VC2EvidenceValidator.or(VC2EvidenceValidator.array()).optional(),
})
.catchall(z.any());
export type UnsignedVC = z.infer<typeof UnsignedVCValidator>;
export const VCValidator = UnsignedVCValidator.extend({
proof: ProofValidator.or(ProofValidator.array()),
});
export type VC = z.infer<typeof VCValidator>;
Graph of Plugins
Last updated
Was this helpful?