@aimform/apps
v0.1.2
Published
Aimform App Store SDK — define, validate, and publish apps for the Aimform platform
Maintainers
Readme
@aimform/apps
The official SDK for building, validating, and publishing apps on the Aimform platform.
@aimform/apps gives third-party developers everything they need to define app manifests, entity types, tools, semantic Capability implementations, onboarding flows, and profile sections — all with full TypeScript support and runtime validation via Zod.
The package also owns the pure platform-ingestion boundary. AppManifestInput
and validateAppManifestInput() preserve the permissive shape accepted by the
platform upload route, while projectAppManifest() converts scopes, tools, and
entity declarations, and Function-level semantic declarations into provider
capabilities, schema projections, and CapabilityImplementation records. The
API persists those projections but does not redefine their semantics.
Authentication may be oauth2, api_key, basic, bearer, mTLS, custom,
or none. Authentication configuration describes the connection UI and
provider protocol only; credential material is supplied when a Connection is
created and is stored behind an opaque Vault handle, never in the manifest.
Aimform-configured Apps can declare connectionOptions to expose a
self-service BYOD path while keeping the managed Aimform Connection as the
default. BYOD fields are Form Intake metadata only: every field is marked
secret, values are validated at connection time, and the resulting material is
stored in Vault. manualSteps documents provider actions such as creating an
API application or adding an egress allowlist. The catalog and chat setup
surface expose the billing mode and these instructions, but never credential
values.
connectionOptions: {
defaultMode: "managed",
managed: { enabled: true, billingLabel: "Billed through Aimform" },
byod: {
enabled: true,
credentialFields: [
{ key: "apiKey", label: "API key", inputType: "password", secret: true, required: true },
],
manualSteps: [{
id: "create-key",
title: "Create a restricted key",
description: "Create only the provider permissions this App needs.",
}],
},
},Connection credentials are represented by ConnectionCredentialHandle and
ConnectionCapabilityGrant metadata. Functions and Workflows must receive
explicit capability grants rather than inheriting every capability of a
Connection.
The public catalog includes provider-specific DataImpulse, Decodo,
IPRoyal, and Webshare Apps. Each uses the shared
browser.egress.residential capability and exposes managed/BYOD connection
metadata. Browser Use and other Apps/Tools consume the provider-neutral
capability rather than coupling to a vendor. BYOD proxy endpoint, username,
password, and optional rotation fields are stored behind Vault; the App
catalog and connection health API only return safe status and billing
metadata.
ManagedProviderProfile is host-owned account configuration used to power a
managed Connection. Its secretRef is an opaque vault:// reference and is
never included in customer-facing Connection or runtime responses.
Apps declare executable Functions and their required capabilities. A selected
Function may be projected to AI with exposedToAi; the Workflow Specification
and its durable coordination nodes remain an @aimform/workflows concern.
Use findAppEvent() and findAppWebhook() when an App runtime needs to map a
provider callback to a declared normalized event. The API verifies the
Connection-scoped signature and emits the resulting event through
@aimform/workflows.
App Manifest v2
Manifest v2 packages the complete public integration contract in one App:
const app = defineApp({
// identity, entryPoint, auth, entity types, scopes, and onboarding...
id: "warehouse",
name: "Warehouse",
version: "1.0.0",
avatar: "https://example.com/warehouse.png",
description: "Warehouse operations",
category: "operations",
entryPoint: { type: "external_call", url: "https://warehouse.example.com/functions" },
auth: { type: "api_key", name: "X-Warehouse-Key" },
declaresEntityTypes: [{ name: "shipment", properties: [{ name: "id", type: "string" }] }],
scopes: { read: ["shipment"] },
functions: [
{
name: "getShipment",
description: "Read a shipment",
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
outputSchema: { type: "object" },
requiredCapabilities: ["shipment.read"],
implements: [{ capability: "aimform://capabilities/logistics.shipment.get@^1" }],
requires: [{ capability: "aimform://capabilities/logistics.shipment.lookup@^1" }],
riskTier: "auto",
exposedToAi: true,
idempotent: true,
},
],
events: [{ name: "warehouse.shipment.updated" }],
webhooks: [{ id: "shipments", path: "/webhooks/shipments", events: ["warehouse.shipment.updated"] }],
egress: { allowlist: ["warehouse.example.com"] },
rateLimits: { requests: 60, windowSeconds: 60, keyBy: "connection" },
onboarding: { steps: [{ id: "connect", type: "api_key_form", title: "Connect Warehouse" }] },
});Functions are the canonical executable App operation. A Function may declare
semantic implements and requires entries: the App owns the resulting
implementation metadata, while the referenced Capability contract remains
owned by its Capability catalog. Aimform projects only
Functions with exposedToAi: true into the compatibility AI-tool registry.
When implements is present, that semantic contract is also the Function's
catalog identity; the legacy function:${name} capability is not projected.
Name-shaped capabilities remain only for older manifests that have no semantic
implementation declaration.
Implementation declarations may include deterministic input/output mappings
({ version, rules }) from @aimform/capabilities; hosted conformance runs
evaluate those mappings against canonical fixtures without serializing code.
Exact versioned Capability URIs are retained as capabilityVersion metadata so
compatible Journey requests such as ^1 resolve correctly.
Connection-scoped invocation validates the Function input schema, checks the
explicit Connection grant, resolves credentials inside Vault, enforces egress
and rate-limit policy, and returns normalized upstream errors without exposing
credential material.
Sandboxed generated Apps
entryPoint.type === "sandboxed_code" carries immutable sandboxedCode
metadata: a SHA-256 source digest, optional artifact reference, language,
entry function, and egress policy. The API dispatches source-backed candidates
to the dedicated Compute Worker, where code runs with default-deny egress,
bounded input/output, timeout, and Guard admission. Artifact-backed execution
remains fail-closed until the blob/package runtime is enabled.
The generation seam creates private candidates, runs bounded tests, and repairs by producing new immutable versions. Candidate test success is not a substitute for Capability Verification or binding; those remain explicit control-plane gates.
Public catalog Apps
The package ships the first reusable public App contracts through the same manifest and publishing path used by third-party Apps:
import {
PUBLIC_AIMFORM_APP_MANIFESTS,
SCALEFORCE_WEBSHOP_APP_MANIFEST,
} from "@aimform/apps/public";
PUBLIC_AIMFORM_APP_MANIFESTS.map((app) => app.id);
// ["cloudflare", "dataimpulse-residential-proxy", "decodo-residential-proxy", "iproyal-residential-proxy", "webshare-residential-proxy", "gocardless", "otp-bank", "aliexpress", "resend"]
SCALEFORCE_WEBSHOP_APP_MANIFEST.id;
// "scaleforce-webshop" — private Organisation AppThe public manifests declare canonical Functions, entity types, events, connection authentication, egress, rate limits, and health checks. They do not contain API keys, OAuth tokens, certificates, or other credential material. The API projects them into the published catalog idempotently; an Organisation can discover and install them through the normal App connection flow.
The package also owns the portable provider clients used by the API runtime.
They accept an injected fetch implementation and never persist secrets:
import { GoCardlessProvider } from "@aimform/apps";
const client = new GoCardlessProvider({ fetchFn });
const tokens = await client.createAccessToken({ secretId, secretKey });
const institutions = await client.listInstitutions(tokens.accessToken, "GB");GoCardless uses the server-side token → end-user agreement → requisition redirect flow. AliExpress uses seller OAuth and signed Open Platform requests. The API keeps deployment credentials in Worker secrets and stores each Organisation's resulting connection tokens behind a Vault handle.
AliExpressCommerceProvider adapts the AliExpress App Function surface to the generic CommerceProvider port from @aimform/capabilities. It maps product, freight, supplier-order, and shipment responses into the shared contract shape while retaining the managed Connection as the only credential boundary. A Procurement Skill can therefore replace AliExpress with another App through a Capability Binding without changing its policy or orchestration source.
GoCardless authorizations are independent Connections, so one Organisation can connect multiple banks. Its account, balance, transaction, detail, and refresh Functions are invoked through the selected Connection and cannot read another Connection's accounts.
After a consent is completed, GoCardless may take a short time to prepare the
account snapshot. The provider client polls the account resource and retries a
409 readiness response a bounded number of times; rate-limit responses are
returned without retrying so a provider quota is not consumed repeatedly.
Transaction reads without an explicit date filter request the full 90-day
consent window (date_from and date_to), matching the reliable ingestion
flow used by Spendbook instead of relying on a bank-specific default window.
Use runManifestConformance() for package-only checks. The API upload and
publication path also runs the manifest scan; the hosted App conformance test
covers migration, public discovery, cross-Organisation installation, Function
execution, disconnect/reconnect, capability grants, and rate limiting.
Installation
npm install @aimform/apps
# or
pnpm add @aimform/appsQuick Start
Use defineApp() to create a complete app manifest:
import { defineApp } from "@aimform/apps";
const myApp = defineApp({
id: "my-slack-bot",
name: "Slack Bot",
version: "1.0.0",
avatar: "https://example.com/slack-icon.png",
description: "Bring Slack messages into Aimform.",
category: "communication",
entryPoint: {
type: "mcp",
url: "https://slack-bot.example.com/mcp",
},
auth: {
type: "oauth2",
provider: "slack",
},
declaresEntityTypes: [
{
name: "slack_message",
properties: [
{ name: "text", type: "text", label: "Message", required: true },
{ name: "channel", type: "string", label: "Channel" },
{ name: "sent_at", type: "date", label: "Sent" },
],
},
],
scopes: {
read: ["slack_message"],
write: ["slack_message"],
},
tools: [
{
name: "send_slack_message",
description: "Send a message to a Slack channel",
riskTier: "confirm",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
channel: { type: "string" },
text: { type: "string" },
},
required: ["channel", "text"],
},
},
],
onboarding: {
steps: [
{
id: "connect-slack",
type: "oauth_connect",
title: "Connect your Slack workspace",
provider: "slack",
},
],
},
profile: {
sections: [
{
id: "recent-messages",
type: "view",
title: "Recent Messages",
entityType: "slack_message",
fields: ["text", "channel", "sent_at"],
defaultSort: "sent_at",
},
],
},
});Entity Types
Entity types define the data model your app brings into Aimform. Use the fluent builder or inline array syntax:
import { defineEntityType } from "@aimform/apps";
// Fluent builder
const taskEntity = defineEntityType("task")
.addProperty("title", "string", { label: "Title", required: true, searchable: true })
.addProperty("status", "string", { label: "Status" })
.addProperty("due_date", "date", { label: "Due Date" })
.build();
// Inline array
const noteEntity = defineEntityType("note", [
{ name: "title", type: "string", label: "Title", required: true },
{ name: "content", type: "text", label: "Content", searchable: true },
{ name: "created_at", type: "date", label: "Created" },
]);Tools
Tools are actions exposed by your app that users and the AI assistant can invoke:
import { defineTool } from "@aimform/apps";
// Safe read-only tool (auto-executable)
const searchTasks = defineTool({
name: "search_tasks",
description: "Search tasks by title or status",
riskTier: "auto",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
},
required: ["query"],
},
});
// Write operation (requires user confirmation)
const createTask = defineTool({
name: "create_task",
description: "Create a new task",
riskTier: "confirm",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Task title" },
dueDate: { type: "string", description: "Due date (ISO 8601)" },
},
required: ["title"],
},
});
// Blocked tool (requires explicit user action)
const deleteTask = defineTool({
name: "delete_task",
description: "Permanently delete a task",
riskTier: "blocked",
exposedToAi: false,
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task ID" },
},
required: ["taskId"],
},
});Risk Tiers
| Tier | Behavior |
|------|----------|
| auto | Executes automatically without user confirmation. Use for safe read-only operations. |
| confirm | Requires user confirmation before execution. Use for write/delete operations. |
| blocked | Never auto-executes. Requires explicit user trigger. Use for dangerous or irreversible actions. |
Onboarding Steps
Onboarding steps guide users through connecting your app:
import { defineOnboardingStep } from "@aimform/apps";
// OAuth2 connection
const connectStep = defineOnboardingStep({
id: "connect",
type: "oauth_connect",
title: "Connect your account",
description: "Authorize access to your data.",
provider: "google",
});
// API key entry
const apiKeyStep = defineOnboardingStep({
id: "api-key",
type: "api_key_form",
title: "Enter your API key",
description: "Find your API key in your account settings.",
apiKeyLabel: "API Key",
apiKeyHelpText: "Paste the API key from your provider dashboard.",
});
// Permission review
const reviewStep = defineOnboardingStep({
id: "review",
type: "permission_review",
title: "Review permissions",
description: "This app will be able to read your emails.",
});
// Custom setup
const customStep = defineOnboardingStep({
id: "setup",
type: "custom",
title: "Configure your workspace",
customUrl: "https://my-app.example.com/setup",
});Profile Sections
Profile sections display your app's data in Aimform entity detail views:
import { defineProfileSection } from "@aimform/apps";
// Table/list view
const listView = defineProfileSection({
id: "recent-items",
type: "view",
title: "Recent Items",
entityType: "task",
fields: ["title", "status", "due_date"],
defaultSort: "due_date",
});
// Document view (rich content)
const docView = defineProfileSection({
id: "note-content",
type: "document",
title: "Note",
entityType: "note",
documentType: "text/markdown",
});
// Freeform board
const board = defineProfileSection({
id: "kanban",
type: "space",
title: "Project Board",
entityType: "task",
});Full Gmail App Example
A complete Gmail app manifest using all the SDK features:
import {
defineApp,
defineEntityType,
defineTool,
defineOnboardingStep,
defineProfileSection,
defineScopes,
} from "@aimform/apps";
const gmail = defineApp({
id: "gmail",
name: "Gmail",
version: "1.0.0",
avatar: "https://example.com/gmail-icon.png",
description: "Read, search, and send emails from Gmail.",
category: "communication",
entryPoint: {
type: "mcp",
url: "https://gmail-app.example.com/mcp",
},
auth: {
type: "oauth2",
provider: "google",
},
declaresEntityTypes: [
defineEntityType("email_thread", [
{ name: "subject", type: "string", label: "Subject", required: true, searchable: true },
{ name: "snippet", type: "text", label: "Snippet", searchable: true },
{ name: "sender", type: "email", label: "Sender" },
{ name: "recipients", type: "json", label: "Recipients" },
{ name: "received_at", type: "date", label: "Received" },
{ name: "is_read", type: "boolean", label: "Read" },
{ name: "labels", type: "json", label: "Labels" },
]) as ReturnType<typeof defineEntityType>,
],
scopes: defineScopes(["email_thread"], ["email_thread"]),
tools: [
defineTool({
name: "search_emails",
description: "Search Gmail messages by query",
riskTier: "auto",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Gmail search query (supports Gmail search operators)" },
maxResults: { type: "number", description: "Maximum results to return", default: 10 },
},
required: ["query"],
},
}),
defineTool({
name: "get_email",
description: "Get full content of a specific email",
riskTier: "auto",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
emailId: { type: "string", description: "Email message ID" },
},
required: ["emailId"],
},
}),
defineTool({
name: "send_email",
description: "Send an email via Gmail",
riskTier: "confirm",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
to: { type: "string", description: "Recipient email address(es)" },
cc: { type: "string", description: "CC recipients" },
subject: { type: "string", description: "Email subject" },
body: { type: "string", description: "Email body (plain text or HTML)" },
},
required: ["to", "subject", "body"],
},
}),
defineTool({
name: "delete_email",
description: "Delete an email (moves to trash)",
riskTier: "confirm",
exposedToAi: true,
inputSchema: {
type: "object",
properties: {
emailId: { type: "string", description: "Email message ID" },
},
required: ["emailId"],
},
}),
],
onboarding: {
steps: [
defineOnboardingStep({
id: "connect-gmail",
type: "oauth_connect",
title: "Connect your Gmail account",
description: "We'll redirect you to Google to authorize access to your Gmail.",
provider: "google",
}),
defineOnboardingStep({
id: "review-permissions",
type: "permission_review",
title: "Review permissions",
description: "This app needs access to read, search, and send emails from your Gmail account.",
}),
],
},
profile: {
sections: [
defineProfileSection({
id: "recent-emails",
type: "view",
title: "Recent Emails",
entityType: "email_thread",
fields: ["subject", "snippet", "sender", "received_at", "is_read"],
defaultSort: "received_at",
}),
defineProfileSection({
id: "email-content",
type: "document",
title: "Email Body",
entityType: "email_thread",
documentType: "text/html",
}),
],
},
});Validation
The SDK includes runtime validation via Zod schemas, accessible from the server entry point:
import { validateManifest, scanApp } from "@aimform/apps/server";
// Validate a manifest loaded from JSON
const rawManifest = JSON.parse(
await Deno.readTextFile("aimform.manifest.json"),
);
const result = validateManifest(rawManifest);
if (!result.success) {
console.error("Manifest is invalid:");
for (const error of result.errors) {
console.error(` - ${error}`);
}
process.exit(1);
}
// Deep scan for warnings and blocking issues
const report = scanApp(result.data);
if (!report.passed) {
console.error("Scan found blocking issues:");
for (const error of report.errors) {
console.error(` - ${error}`);
}
}
if (report.warnings.length > 0) {
console.warn("Warnings (non-blocking):");
for (const warning of report.warnings) {
console.warn(` - ${warning}`);
}
}API Reference
Main Entry (@aimform/apps)
| Export | Description |
|--------|-------------|
| defineApp(manifest) | Create a complete app manifest |
| defineEntityType(name, properties?) | Define an entity type (returns builder or declaration) |
| defineTool(config) | Define a tool/action |
| defineOnboardingStep(step) | Define an onboarding step |
| defineProfileSection(section) | Define a profile section |
| defineScopes(read, write?) | Define scoped permissions |
| OAUTH2_PROVIDERS | Constants for known OAuth2 providers |
| InferAppManifest<T> | Infer the type of a specific manifest |
Server Entry (@aimform/apps/server)
| Export | Description |
|--------|-------------|
| validateManifest(input) | Validate unknown input against the manifest schema |
| scanApp(manifest) | Deep scan a manifest for warnings and errors |
| aimformAppManifestSchema | Raw Zod schema for custom validation logic |
Client Entry (@aimform/apps/client)
Type-only exports for browser-safe imports. All manifest types are re-exported:
AimformAppManifest, ToolDeclaration, EntityTypeDeclaration, AppScopes, etc.
Types
AimformAppManifest
The top-level manifest type. See the Quick Start example for full shape.
EntityTypeDeclaration
interface EntityTypeDeclaration {
name: string;
properties: PropertyDefinition[];
}ToolDeclaration
interface ToolDeclaration {
name: string;
description?: string;
riskTier: "auto" | "confirm" | "blocked";
exposedToAi: boolean;
inputSchema?: Record<string, unknown>;
}OnboardingStep
interface OnboardingStep {
id: string;
type: "oauth_connect" | "api_key_form" | "permission_review" | "custom";
title: string;
description?: string;
provider?: string;
apiKeyLabel?: string;
apiKeyHelpText?: string;
customUrl?: string;
}ProfileSection
interface ProfileSection {
id: string;
type: "view" | "document" | "space";
title: string;
entityType?: string;
fields?: string[];
defaultSort?: string;
documentType?: string;
}AuthConfig
interface AuthConfig {
type: "oauth2" | "api_key" | "none";
provider?: string;
}EntryPointConfig
interface EntryPointConfig {
type: "mcp" | "sandboxed_code" | "external_call";
url?: string;
}License
MIT
Provider-backed connection functions accept { guard } in their provider
options. Production hosts must supply the connection-scoped binding so request
and retry budgets are reserved before GoCardless or AliExpress is called.
