@alphayard/appkit
v1.7.0
Published
Reusable AppKit SDK client for Boundary and UniApps services
Readme
@alphayard/appkit
Reusable AppKit SDK client for mobile apps, web apps, and service-to-service integrations.
Features
- hosted or fully client-owned authentication UI
- typed authentication continuation states for MFA, verification, and required factor enrollment
- auth token storage helpers
- branding and Color Studio config helpers
- localization helpers
- identity endpoints
- CMS collection helpers
- file storage helpers
- communication helpers for email and SMS
- organization and invite helpers for admin contexts
- application registry, Personal/Business purpose, and company-website visibility helpers
- AI Studio helpers for published apps, flows, and agents
- emergency contacts, safety alerts, and circle member status/location helpers
- legal acceptance and developer-document feedback helpers
- storage listing, metadata, search, quota, and analytics helpers
- configured social-provider access-token verification
Install
npm install @alphayard/appkitStandard SDK usage
import { AppKit } from '@alphayard/appkit';
const client = new AppKit({
clientId: 'client-id',
domain: 'https://your-appkit-domain.com',
baseURL: 'https://your-appkit-domain.com',
storage: 'memory',
});Authentication UI: hosted or your own
AppKit supports two UI models over the same authentication backend and application policy.
Option A — AppKit hosted UI
import { createHeadlessAppKit } from '@alphayard/appkit/headless-auth';
const appkit = createHeadlessAppKit({
clientId: 'public-client-id',
appId: 'application-uuid',
domain: 'https://auth.example.com',
});
window.location.assign(appkit.auth.getHostedLoginUrl({
redirect: '/oauth/authorize?client_id=public-client-id',
locale: 'en',
mode: 'login',
}));Option B — render your own UI
Use the headless entrypoint when your web/mobile application owns all login screens. It returns authentication continuations as data instead of forcing AppKit UI.
import { createHeadlessAppKit } from '@alphayard/appkit/headless-auth';
const appkit = createHeadlessAppKit({
clientId: 'public-client-id',
appId: 'application-uuid',
domain: 'https://auth.example.com',
// Use your secure platform storage adapter in mobile apps.
storage: 'memory',
});
const result = await appkit.auth.loginWithCredentials({
email,
password,
rememberMe: true,
});
switch (result.status) {
case 'authenticated':
// Token storage has already been updated by the SDK.
navigateToApp();
break;
case 'mfa_required':
showMfaScreen({
challengeToken: result.challengeToken,
channels: result.availableChannels,
});
break;
case 'email_verification_required':
showEmailVerificationScreen(result.verificationToken);
break;
case 'mfa_enrollment_required':
showRequiredSecuritySetup({
enrollmentToken: result.enrollmentToken,
methods: result.enrollmentMethods,
});
break;
default:
// `complete`, `passwordless_challenge`, and `recovery_codes`
// are handled by the matching journey that created them.
break;
}The client does not decide whether MFA, email verification, signup, legal acceptance, password rules, or factor enrollment are required. Those rules come from the registered application's effective AppKit policy.
Load policy and branding for your UI
const config = await appkit.auth.getConfig();
console.log(config.authPolicy); // password/MFA/signup/legal rules
console.log(config.branding); // safe public branding
console.log(config.providers); // production-ready configured methods onlyDo not embed an AppKit client secret in a browser or mobile application. Public headless auth uses the registered clientId/appId; confidential client secrets remain server-only.
Passwordless
const requested = await appkit.auth.requestPasswordless({
method: 'sms-otp',
identifier: '+66812345678',
rememberMe: true,
});
if (requested.status === 'passwordless_challenge') {
const completed = await appkit.auth.verifyPasswordless({
challengeToken: requested.challengeToken,
code: otp,
});
// `completed` can still be mfa_required / email_verification_required /
// mfa_enrollment_required. Render the returned continuation.
}MFA
if (result.status === 'mfa_required') {
const request = await appkit.auth.requestMfa({
challengeToken: result.challengeToken,
channel: 'email',
});
const completed = await appkit.auth.verifyMfa({
challengeToken: result.challengeToken,
channel: 'email',
otpChallengeId: request.otpChallengeId || undefined,
code: otp,
trustDevice: false,
});
}For passkey MFA, requestMfa() returns publicKey options and a passkeyChallengeToken. Your web/native layer performs the WebAuthn ceremony and passes the serialized credential to verifyMfa().
Required TOTP/passkey enrollment
if (result.status === 'mfa_enrollment_required') {
const setup = await appkit.auth.startMfaEnrollment({
enrollmentToken: result.enrollmentToken,
method: 'totp',
});
if (setup.method === 'totp') {
// Show setup.otpauthUri as a QR code or setup.secret as a manual key.
const verified = await appkit.auth.verifyTotpEnrollment({
setupToken: setup.setupToken,
code: authenticatorCode,
});
if (verified.status === 'recovery_codes') {
// Display these once and require the user to save them.
console.log(verified.backupCodes);
// Continue using verified.next.
}
}
}Passkeys with client-owned UI
The SDK core intentionally does not call navigator.credentials; it exposes raw WebAuthn options so web and native clients can use their platform-native ceremony.
const options = await appkit.auth.getPasskeyAuthenticationOptions();
const credential = await yourPlatformWebAuthnGet(options.publicKey);
const result = await appkit.auth.verifyPasskeyAuthentication({
challengeToken: options.challengeToken,
credential,
rememberMe: true,
});For browser deployments on a different site from the AppKit RP domain, WebAuthn RP/origin rules still apply. Use an RP/domain configuration compatible with your deployment or use the AppKit-hosted passkey ceremony; do not relax WebAuthn origin verification.
Password recovery
Password reset requires both the signed reset challenge and its OTP. A reset token is never accepted as the OTP itself.
const reset = await appkit.auth.resetPassword({
resetToken,
otp,
password: newPassword,
});Browser CORS
When a browser UI calls a different AppKit origin directly, that UI origin must be included in the deployment's CORS_ORIGIN allowlist. Server-to-server and native clients do not depend on browser CORS. A same-origin backend proxy is also supported.
Application registry, purpose, and company website visibility
Purpose is application-level metadata with two values: personal or business. Company website visibility is a separate global boolean and does not change access mode, activation, lifecycle stage, or environment configuration.
import { ApplicationRegistryClient } from '@alphayard/appkit/applications';
const applications = new ApplicationRegistryClient({
baseURL: 'https://your-appkit-domain.com',
// Optional for admin metadata reads/writes in service-to-service usage.
accessToken: process.env.APPKIT_ADMIN_TOKEN,
});
// Normal public registry. Planning-stage applications remain excluded by AppKit.
const registry = await applications.listPublic({
organizationId: 'organization-id',
});
console.log(registry.applications[0]?.purposeType); // personal | business
console.log(registry.applications[0]?.showOnCompanyWebsite); // boolean
// Recommended for a company website: only return applications enabled for that surface.
const companyWebsiteApps = await applications.listPublic({
organizationId: 'organization-id',
companyWebsiteOnly: true,
});
const purpose = await applications.getPurpose('application-id');
await applications.setPurpose('application-id', 'personal');
const visibility = await applications.getCompanyWebsiteVisibility('application-id');
await applications.setCompanyWebsiteVisibility('application-id', true);In browser admin contexts, cookie authentication is used by default. Legacy applications without an explicit company-website visibility value keep their prior visible behavior; newly created applications default to hidden until enabled.
AI Studio
const { aiConfig, apps } = await client.ai.list();
const events = await client.ai.runFlow({
flowId: 'flow-id',
inputVariables: { message: 'Summarize my account' },
});
for await (const event of events) {
console.log(event.data);
}Color Studio
const colorConfig = await client.getColorStudioConfig();
const updated = await client.updateColorStudioConfig({
...colorConfig.branding,
tokens: {
...colorConfig.branding.tokens,
activeColorThemeId: 'core-theme',
},
});
console.log(updated.colorStudio.activeTheme);await client.updateColorStudioVariable({
key: 'chat.accent',
value: '#06B6D4',
});
await client.updateColorStudioGroup({
groupId: 'chat',
group: { name: 'Chat', description: 'Conversation colors.' },
});
await client.setActiveColorTheme({ themeId: 'core-theme', makeDefault: true });
await client.duplicateColorTheme({ themeId: 'core-theme', name: 'Spring Theme' });Color Studio writes go through the admin application API, so the SDK must be configured with appId and an admin-capable authenticated context.
Localized application description
The public application description is registered under application.description in the application's active Localization Studio package. The SDK resolves exact and regional language codes and falls back through the application's configured fallback language:
const thaiDescription = await client.getApplicationDescription('th-TH');
// Equivalent namespaced API:
const description = await client.localization.getApplicationDescription('en');Application previews
Branding responses include typed desktop and mobile preview galleries:
const branding = await client.branding.getMobileBranding();
console.log(branding.previewImages?.desktop);
console.log(branding.previewImages?.mobile);Each gallery contains up to 10 uploaded image URLs.
Organizations
Organization helpers use the authenticated admin API, so the SDK needs an admin-capable token.
const { organizations } = await client.organizations.getMine();
const organization = await client.organizations.create({
name: 'Acme Studio',
website: 'https://acme.example',
});
await client.organizations.updateProfile(organization.id, {
logoUrl: '/uploads/acme-logo.png',
contact: { email: '[email protected]' },
});
const invite = await client.organizations.createInvite({
organizationId: organization.id,
role: 'member',
});Mobile Splash
const splash = await client.getSplashConfig();
console.log(splash?.logoUrl);
console.log(splash?.variants.dark.backgroundImageUrl);Build tooling can fetch the same unauthenticated config directly:
curl "https://your-appkit-domain.com/api/v1/mobile/splash?app_slug=my-app"