XTP for JavaScript
npm install @dylibso/xtpThe JS SDK for XTP.
The first step to integrating with XTP is to create a Client:
``js
import createClient from '@dylibso/xtp'
const client = await createClient({
appId: process.env.APP_ID, // looks like: 'app_xxxxxxxxxxxxxxxxxxxxxxxx',
token: process.env.XTP_TOKEN, // looks like: 'xtp0_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
logger: console,
useWasi: true
})
`
You'll need your appId and a token.
> Note: XTP_TOKEN is a secret and is sensitive. Don't store the TOKEN in source code.
The client instance holds the connection to the API and any plugins you may want to run.
For this reason you probably want to share the instance across your whole application.
If you need to share this across multiple modules, you should use a singleton constructor:
`js
// xtpClient.js
let xtpClient = null;
export async function getXtpClient() {
if (!xtpClient) {
xtpClient = await createClient({
// your client options here
})
}
return xtpClient
}
// mymodule.js
import { getXtpClient } from './xtpClient'
const xtpClient = await getXtpClient()
`
If your plug-in developers may be installing multiple plug-ins on an extension-point,
you'll need a binding name. If not you can skip this step. You can ask which plugins are
bound to an extension-point with the listAvailablePlugins
`js
const GUEST_KEY = 'acme-corp'
const EXT_NAME = 'MyExtensionPoint'
const pluginNames = await xtpClient.listAvailablePlugins(EXT_NAME, GUEST_KEY)
// ['plugin1', 'plugin2']
`
Now you can actually execute the code. This next step will do a few things for you:
1. Fetch the latest plug-in from XTP
2. Cache it on disk
3. Compile and load it
4. Cache it in memory
5. Execute the function
`jsmyExport
// assuming your extension point has an export
const result = await client.extensionPoints.MyExtensionPoint.myExport(
GUEST_KEY,
'input data', // depends on what plug-in expects
{
bindingName: 'plugin1', // optional item from the list of names we got in the following step
default: 'default value' // can be an object, Uint8Array, or string
}
)
console.log(result)
`
---
For local plugin development, the SDK supports filesystem mode which enables hot reload capabilities without requiring an internet connection or XTP service. The filesystem mode uses lazy loading - extension points and functions are discovered automatically when accessed.
`js
import createClient from '@dylibso/xtp'
const client = await createClient({
filesystem: {
pluginDir: './plugins', // Directory containing your .wasm files
watch: true, // Enable hot reload (default: false)
watchDebounceMs: 100 // Debounce time for file changes
},
logger: console,
useWasi: true
})
// Use exactly like regular XTP client - functions are discovered automatically
await client.extensionPoints.myExtension.process('guestKey', inputData)
`
Organize plugins by extension point name. Each subdirectory becomes an extension point:
``
plugins/
├── my-extension/
│ ├── guest1.wasm # Plugin for specific guest "guest1"
│ ├── guest2.wasm # Plugin for specific guest "guest2"
│ └── default.wasm # Fallback for any guest key
└── another-extension/
└── default.wasm # Fallback for any guest key
The SDK automatically:
- Discovers extension points from directory names
- Maps guest keys to {guestKey}.wasm files or falls back to default.wasm
- Discovers function exports at runtime when called
- Handles missing functions with natural Extism errors
Guest Key Notes:
- Guest keys typically align with external user IDs (e.g., "user123", "acme-corp")
- For testing, you can hard-code any guest key (e.g., "test", "dev", "local")
- Files are resolved as: {guestKey}.wasm → default.wasm → not found
1. Build your plugin: cargo build --target wasm32-wasi --releasecp target/wasm32-wasi/release/my_plugin.wasm ./plugins/my-extension/default.wasm
2. Copy to plugins directory:
3. SDK automatically detects changes and reloads the plugin
4. No restart or metadata updates required - next function call uses the updated plugin
Functions are discovered automatically from the WASM exports - no configuration needed.
Remove the filesystem option and add your XTP credentials:
`js`
const client = await createClient({
// Remove for production
// filesystem: { ... },
// Add production config
baseUrl: 'https://xtp.dylibso.com',
token: 'your-token',
appId: 'your-app-id',
logger: console,
useWasi: true
})
---
Create a client with the provided options. In API mode, the ClientcreateClient
fetches the available list of extension points from the XTP API. In filesystem mode,
extension points are discovered lazily from the directory structure. If API calls
fail the promise returned by will reject.
#### interface XTPClientOptions
- baseUrl: defaults to https://xtp.dylibso.com.token
- : An XTP API Token.functions
- : A {[string]: {[string]: CallableFunction}} map to be exposed to modules.logger
- : A pino-compatible logger.keepResidentMs
- : The number of milliseconds to keep plugins "live" after a call. This means theXTP_KEEP_RESIDENT_MS
plugin is in-memory and subsequent calls to the same extension point and guest tag will re-use the
plugin instance. Resets after each call. Defaults to environment variable , or5000ms
.refetchAfterMs
- : The number of milliseconds to treat local cached plugin data as "fresh"; after whichuseWasi
the client will "re-fetch" the installed plugin for the given extension point and tag to revalidate the
cache.
- : boolean, defaults to true -- whether or not to enable WASIp1 for guests.storage
- : Any ExtensionStorage interface. Uses localStorage on Deno, cacache on Node.fetch
- : Any fetch-compatible function for making requests.filesystem
- : For offline development mode. Object with:pluginDir
- : Directory path containing plugin subdirectorieswatch
- : Enable hot reload file watching (default: false)watchDebounceMs
- : Debounce time for file change detection (default: 100ms)
---
#### fn client.clone(opts: Partial
Clone an existing client, bypassing the "load extension points" call during
typical startup. Options for the client can be partially overridden.
#### fn close(): Promise
Close all "live" plugins. May be called repeatedly.
#### fn inviteGuest(opts: RegisterGuest): Promise
Invite a guest to develop and install plugins on your XTP application.
#### interface RegisterGuest
- email: string: The guest's email address.name: string
- : The human-readable name of the guest.guestKey: string
- : A unique string held by your application to identify the guest.
#### prop extensionPoints
#### fn extensionPoints[extName: string]exportName: string: Promise
`js`
const result = await client.extensionPoints.foo.bar('my guest key', {hello: 'world'}, { bindingName: 'my-plugin', default: 'default value' })
Call a plugin export installed by a guest (identified by guestKey) at an
extension point.
opts:
Use the bindingName to idenfity the named plugin to return. Defaults to "default".default
Use optional to return a default value
---
Implement your own extension storage for fun and profit! Reach for this
interface if the default storage options aren't working for you -- if you'd
like to store plugins in a database, for example.
#### fn getByExtIdGuestKey(extId: string, guestKey: string): Promise
Fetch plugin content based on an extId/guestKey pair. Must refer to theStoredPlugin
same content as returned by getByETag.
Return null if not present.
#### fn getByETag(etag: string): Promise
Fetch plugin content based on the ETag value returned by fetchingStoredPlugin
installations.
Must refer to the same content as returned by getByExtIdGuestKey.
Return null if not present.
#### fn store(extId: string, guestKey: string, etag: string, meta: Record
Store plugin content, indexed by both (extId, guestKey) and etag.
#### interface StoredPlugin
- metadata: Record: Metadata about the record. Should includeetag
, content-type, and last (a number representing the millisecondsdata: Uint8Array
since unix epoch at which the plugin content was last stored.)
- : The plugin content, suitable for passing to WebAssembly.compile.size: number`: The size of the stored data.
-
---