Reference
@ledable/sdk/app
Define an app, its settings, and what it returns.
On this page
Generated from the types of the SDK. Import from @ledable/sdk/app; an editor shows the same declarations and comments as you type.
Functions
account
A third-party account linked through AuthHub; the render receives its handle.
function account<
const S extends Common & {
readonly platform: "github" | "todoist" | "youtube" | "spotify";
},
>(spec: S)assetsPrefix
Prefix of every asset of one version; the asset path is appended after a slash.
function assetsPrefix(appId: string, version: string): stringboolean
function boolean<const S extends Common & { readonly default?: boolean }>(spec: S)bundleKey
function bundleKey(appId: string, version: string): stringcacheTag
Digest composite application keys without exposing credentials in cache paths.
function cacheTag(...parts: readonly unknown[]): Promise<string>colorRgb
function colorRgb<
const S extends Common & { readonly default?: readonly [number, number, number] },
>(spec: S)colorRgba
function colorRgba<
const S extends Common & { readonly default?: readonly [number, number, number, number] },
>(spec: S)country
function country<const S extends Common & { readonly default?: string }>(spec: S)createAppCache
function createAppCache(backend: CacheBackend, scope: AppCacheScope): AppCachecreateAppServer
Build a fetch handler speaking the app protocol: GET /, GET /{endpoint}, GET /{endpoint}/render, GET /{endpoint}/preview. Platform-neutral: usable as a Cloudflare Worker fetch export or with any Request/Response-based host.
function createAppServer(options: AppServerOptions): FetchHandlercreateRng
function createRng(seed: number): Rngcredential
A credential the user types in, such as a personal API token, which AuthHub keeps encrypted. The render receives its handle and reads the value with `ctx.credentials.get`; an app with one checks what the user types (`verifyCredential` in ./app.ts).
function credential<const S extends Common>(spec: S)date
function date<const S extends Common & { readonly default?: string }>(spec: S)datetime
function datetime<const S extends Common & { readonly default?: number }>(spec: S)defineApp
function defineApp<S extends Settings>(app: AppDefinition<S>): LedAppfixedNumber
function fixedNumber(value: number, digits = 0): stringfloat
function float<const S extends Common & { readonly default?: number }>(spec: S)hostApp
A bundle run in this process, such as in tests or a local preview, as the host serves it.
function hostApp(app: LedApp, manifest: AppManifest): HostedAppinteger
function integer<const S extends Common & { readonly default?: number }>(spec: S)listingProblems
What a listing holds beyond `LISTING_LIMITS`, one sentence each; empty when it fits.
function listingProblems(listing: Pick<
ReleaseManifest, "name" | "description" | "description_long" | "tags" | "screenshots"
>): string[]location
function location<const S extends Common & { readonly default?: LocationValue }>(spec: S)manifestKey
function manifestKey(appId: string, version: string): stringmanifestToPublicDict
Mirrors models/app.py AppManifest.to_public_dict (non-repo mode).
function manifestToPublicDict(
manifest: AppManifest,
baseUrl: string,
endpoint: string,
): PublicManifestmarquee
function marquee(options: MarqueeOptions): MarqueememoryCacheBackend
In-memory backend for tests and local hosts.
function memoryCacheBackend(now: () => number = Date.now): CacheBackendmultiselect
function multiselect<
const S extends Common & {
readonly default?: readonly string[];
readonly options: readonly string[];
},
>(spec: S)outboundRefusal
Why an app may not request `url`, or null when it may: only HTTPS on the default port to a declared host.
function outboundRefusal(url: URL, network: AppNetwork): string | nullparseFields
Resolve raw string values against the manifest fields: absent optional fields fall back to their default, absent required fields fail. Fields whose options or visibility follow another field are settled once the others are parsed; a hidden one takes its default whatever was sent.
function parseFields(
raw: Readonly<Record<string, string>>,
fields: Readonly<Record<string, ConfigField>>,
): AppParamsparseFieldValue
function parseFieldValue(field: ConfigField, raw: string): ConfigValuereadSettings
What the version's `settings` return, checked, and kept for as long as the app says. Throws when they fail or return what no client could show.
async function readSettings(
app: HostedApp,
endpoint: string,
manifest: AppManifest,
cacheBackend: CacheBackend,
capabilities: SettingsCapabilities,
): Promise<ProjectSettings>resultWebpBlob
Mirrors models/webp.py WebpBlobMixin.get_webp_blob.
async function resultWebpBlob(
image: ResultImage | null | undefined,
frameMs: number,
behavior: AnimationBehavior,
encoder?: FrameEncoder,
): Promise<Uint8Array | null>select
One of `options`, or with `depends_on`, one of `options_by` that field's value. The render receives a string: what is on offer is what the settings return now, which no type written in the code can promise, and the platform has already checked the value against it.
function select<const S extends SelectSpec>(spec: S)significantNumber
Python's general format, including significant-digit rounding and exponent thresholds.
function significantNumber(value: number, digits = 4): stringstableStringify
Deterministic JSON with recursively sorted object keys.
function stableStringify(value: unknown): stringstaticConfigSource
Fixed values layered over the query string; useful as a mock.
function staticConfigSource(values: Readonly<Record<string, string>>): ConfigSourcestring
function string<const S extends Common & { readonly default?: string }>(spec: S)time
function time<const S extends Common & { readonly default?: string }>(spec: S)timezone
function timezone<const S extends Common & { readonly default?: string }>(spec: S)verifyAppCredential
The app's verdict on a credential a user typed in, or null for a bundle without a check. The value and the app's secrets are masked in the verdict's message and in what the check throws, which means the credential could not be checked now.
async function verifyAppCredential(
app: HostedApp,
endpoint: string,
credential: CredentialToVerify,
secretsPool: Readonly<Record<string, string>>,
): Promise<CredentialVerdict | null>versionId
function versionId(appId: string, version: string): stringConstants
APP_CATEGORIES
The store's categories. An app declares exactly one in ledable.json; the store groups its home page by them and refuses a new version without one.
const APP_CATEGORIES = [
"clock",
"weather",
"finance",
"news",
"sports",
"productivity",
"developer",
"entertainment",
"science",
"travel",
] as constappCategorySchema
const appCategorySchema = z.enum(APP_CATEGORIES)appIdSchema
Identity of a published bundle version and the write-once R2 layout the gateway reads it from (docs/architecture/app-runtime.md). The store is the only program writing these keys, so both sides share one definition.
const appIdSchema = z.string().check(z.regex(/^[a-z0-9][a-z0-9-]{0,63}$/))appManifestSchema
const appManifestSchemaassetPathSchema
Paths of plain segments; each starts with a letter or digit, so `..` cannot occur.
const assetPathSchema = z
.string()
.check(
z.maxLength(256),
z.regex(new RegExp(`^${ASSET_SEGMENT}(/${ASSET_SEGMENT})*\\.(${ASSET_EXTENSIONS})$`)),
)assetRefSchema
const assetRefSchema = z.object({ path: assetPathSchema })credentialVerdictSchema
What an app's check of a credential the user typed in found (`verifyCredential`).
const credentialVerdictSchema = z.union([
z.object({ valid: z.literal(true) }),
z.object({ valid: z.literal(false), message: z.string() }),
])DEFAULT_FRAME_MS
const DEFAULT_FRAME_MS = 200DEFAULT_HARD_TTL_MS
const DEFAULT_HARD_TTL_MS = 10 * 60 * 1000DEFAULT_MARQUEE_FPS
const DEFAULT_MARQUEE_FPS = 50DEFAULT_SETTINGS_TTL_MS
How long the platform keeps what `settings` returned, unless the app says otherwise.
const DEFAULT_SETTINGS_TTL_MS = 24 * 60 * 60 * 1000DEFAULT_SOFT_TTL_MS
const DEFAULT_SOFT_TTL_MS = 5 * 60 * 1000hostPatternSchema
`api.example.com` names one host; `*.example.com` names every subdomain of `example.com`, at any depth, but not `example.com` itself. A wildcard over a public suffix would let an app reach hosts of anyone, so `*.com` and `*.github.io` are refused, like a bare `*`.
const hostPatternSchema = z.string().check(
z.refine(pattern => {
const name = pattern.startsWith("*.") ? pattern.slice(2) : pattern;
return name.length <= 253 && HOST_NAME.test(name);
}, "Use a lowercase host name, or *. and a host name, without scheme, port or path"),
z.refine(
pattern => !pattern.startsWith("*.") || !isPublicSuffix(pattern.slice(2)),
"A wildcard must not cover a public suffix such as *.com",
),
)LISTING_LIMITS
How much a store listing holds, in characters, so every store client can lay it out. The CLI checks a project against it before it builds, and the store checks each upload again. Published versions are not held to it, so it can change without a manifest no longer reading.
const LISTING_LIMITS = {
name: 30,
description: 120,
readme: 4000,
tags: 5,
tag: 20,
screenshots: 6,
} as constlocationValueSchema
const locationValueSchemaMAX_MARQUEE_FRAMES
Frame choreography for scrolling content: N seconds at a fixed fps with integer pixel offsets, within the platform frame budget. A marquee may never emit more than MAX_MARQUEE_FRAMES frames — beyond that the frame time stretches so the requested duration is kept with fewer frames.
const MAX_MARQUEE_FRAMES = 600networkSchema
const networkSchemanewsResponseSchema
const newsResponseSchema = z.extend(newsSummarySchema, {
counts: z.record(z.string(), z.int().check(z.nonnegative())),
})newsSummarySchema
const newsSummarySchema = z.object({
items: z.array(z.string()),
})numericSchema
Upstream APIs serialize decimal values as either JSON numbers or numeric strings.
const numericSchema = z.pipe(
z.union([
z.number(),
z.pipe(z.string().check(z.trim(), z.regex(decimalText)), z.transform(Number)),
]),
z.number(),
)previewResultSchema
const previewResultSchema = z.extend(
z.pick(renderResultSchema, { frameMs: true, behavior: true }),
{ image: z.nullish(resultImageSchema) },
)PROJECT_FILE
The file at the root of every app project, which `projectSchema` reads.
const PROJECT_FILE = "ledable.json"projectFieldSchema
const projectFieldSchemaprojectSchema
The authored file contains declarations and local paths, never executable metadata.
const projectSchemapublicManifestSchema
const publicManifestSchemaqueryConfigSource
const queryConfigSource: ConfigSource = ({ request }) => {
const raw: Record<string, string> = {};
for (const [key, value] of new URL(request.url).searchParams) {
raw[key] = value;
}
return raw;
}releaseManifestSchema
Immutable upload snapshot; local document and media paths have already been resolved. `fields` is what the bundle's settings returned when the version was checked, kept for review and for when they cannot be read.
const releaseManifestSchemarenderResultSchema
const renderResultSchema = z.object({
ok: z.optional(z.boolean()),
msg: optionalString,
image: z.optional(z.nullable(resultImageSchema)),
frameMs: z.optional(z.number()),
behavior: z.optional(animationBehaviorSchema),
realtime: z.optional(z.boolean()),
softTtlMs: z.optional(z.number()),
hardTtlMs: z.optional(z.number()),
cacheId: optionalString,
})REQ_HEADER_CACHE_ID
const REQ_HEADER_CACHE_ID = "LD-Cache-ID"REQ_HEADER_DURATION
const REQ_HEADER_DURATION = "LD-Duration"REQ_HEADER_FIRMWARE
const REQ_HEADER_FIRMWARE = "LD-Firmware"REQ_HEADER_INSTANCE
const REQ_HEADER_INSTANCE = "LD-Instance"REQ_HEADER_LANGUAGE
const REQ_HEADER_LANGUAGE = "LD-Language"REQ_HEADER_MODEL
const REQ_HEADER_MODEL = "LD-Model"REQ_HEADER_RESOLUTION
const REQ_HEADER_RESOLUTION = "LD-Resolution"REQ_HEADER_TIMEZONE
const REQ_HEADER_TIMEZONE = "LD-Timezone"RESP_HEADER_BEHAVIOR
const RESP_HEADER_BEHAVIOR = "LD-Behavior"RESP_HEADER_CACHE_ID
const RESP_HEADER_CACHE_ID = "LD-Cache-ID"RESP_HEADER_HARD_TTL
const RESP_HEADER_HARD_TTL = "LD-Hard-TTL"RESP_HEADER_REALTIME
const RESP_HEADER_REALTIME = "LD-Realtime"RESP_HEADER_SOFT_TTL
const RESP_HEADER_SOFT_TTL = "LD-Soft-TTL"secretNameSchema
The name of a secret an app declares and its maintainers set.
const secretNameSchema = z.string().check(z.regex(/^[A-Z_][A-Z0-9_]*$/))settingsSchema
Every setting an app returns (./settings.ts). A field may be shown only for some values of another, or take its options by another's value; that other field is a plain select or switch, so one level of rules settles every field.
const settingsSchemaversionSchema
const versionSchema = z
.string()
.check(z.regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,32})?$/))Classes
ConfigError
class ConfigError extends ErrorInterfaces
AppAssets
interface AppAssets {
/** Raw bytes of any asset, e.g. a `.ledfont` for `decodeBitmapFont`. */
bytes(ref: AssetRef): Promise<Uint8Array>;
/** Decoded pixels of a still WebP asset; for an animation, its first frame. */
image(ref: AssetRef): Promise<Framebuffer>;
/** Every frame of a WebP asset in order; a still asset yields exactly one. */
frames(ref: AssetRef): Promise<readonly Framebuffer[]>;
}AppAuth
interface AppAuth {
/** Current provider access token, or null when authorization is required. */
getToken(handle: string): Promise<string | null>;
}AppCache
interface AppCache {
/** `tag` names the call site (e.g. "quote", "webp") within the app. */
get<T>(tag: string, compute: () => Promise<CacheEntry<T>>, options?: CacheGetOptions): Promise<T>;
}AppCacheScope
interface AppCacheScope {
/** Endpoint name; part of the hard namespace prefix. */
readonly appId: string;
/** Manifest version; keys never survive an app version bump. */
readonly version: string;
readonly fields: readonly string[];
readonly params: AppParams;
}AppCredentials
The credentials users typed into the app's credential settings (./settings.ts).
interface AppCredentials {
/** The value a credential setting's handle names, or null once it is deleted. */
get(handle: string): Promise<string | null>;
}AppImages
interface AppImages {
/** Decode bounded upstream JPEG or PNG image bytes through the host's WASM codecs. */
decode(bytes: Uint8Array): Promise<Framebuffer>;
}AppLocations
interface AppLocations {
/** Resolve coordinates with the platform's versioned geographic timezone dataset. */
timezoneAt(lat: number, lng: number): Promise<string | null>;
}AppNews
interface AppNews {
/** Shared news summaries; provider credentials stay in the host service. */
getSummary(): Promise<NewsResponse>;
}AppServerOptions
interface AppServerOptions {
readonly apps: Readonly<Record<string, HostedApp>>;
readonly configSource?: ConfigSource;
/** Frame compressor for WebP output; defaults to the pure-TS encoder. */
readonly frameEncoder?: FrameEncoder;
/**
* Storage behind ctx.cache, the settings and the encoded WebP; the host decides where entries
* live and how widely they are shared.
*/
readonly cacheBackend: CacheBackend;
/**
* Pool of platform-held secrets available to the hosted apps. Each app only
* ever sees the names its manifest declares.
*/
readonly secrets?: Readonly<Record<string, string>>;
readonly auth?: AppAuth;
/** The credentials users typed into the hosted apps' credential settings. */
readonly credentials?: AppCredentials;
readonly news?: AppNews;
readonly locations?: AppLocations;
readonly images?: AppImages;
/** Asset store of the hosted bundle version; previews read it too. */
readonly assets?: AppAssets;
readonly serverName?: string;
}AssetRef
A build-time reference to one file published next to the bundle. `import art from "./assets/art.webp"` evaluates to this object: the bytes never enter the bundle, and the host reads them from `assets/{appId}@{version}/{path}` in the same write-once store as the bundle.
interface AssetRef {
/**
* Path inside this version's assets: where the file sits in its project, e.g.
* `assets/sun.webp`, or `sdk/<package>/…` for a file of an SDK package.
*/
readonly path: string;
}CacheBackend
Storage backends only see opaque namespaced keys and byte payloads.
interface CacheBackend {
get(key: string): Promise<CachedValue | null>;
put(key: string, value: CachedValue, ttlMs: number): Promise<void>;
}CachedValue
interface CachedValue {
readonly kind: CachedKind;
readonly bytes: Uint8Array;
}CacheEntry
Read-through compute cache for apps. Safety-by-default keying: the cache key is derived by the framework from the app's parsed params — never from caller-supplied strings — so a call site cannot accidentally under-key user-scoped data. Sharing across users is an explicit act: `vary` narrows the key to the named manifest fields, which makes every sharing decision reviewable. The host's backend decides where entries live and how widely they are shared; the gateway's edge cache is per data centre, so each one computes its own. Values must be JSON-serializable or Uint8Array; JSON round-trips lose rich types (Date).
interface CacheEntry<T> {
readonly value: T;
readonly ttlMs: number;
}CacheGetOptions
interface CacheGetOptions {
/**
* Manifest field names the cached value depends on. Omitted = all fields
* (fully isolated per distinct config, the safe default). Narrowing to a
* subset is what enables cross-user sharing.
*/
readonly vary?: readonly string[];
}CredentialToVerify
A credential the user typed into one of the app's credential settings, before it is kept.
interface CredentialToVerify {
readonly field: string;
readonly value: string;
}HostedApp
One version as the protocol host serves it: its static snapshot, and the bundle's methods.
interface HostedApp {
readonly manifest: AppManifest;
settings(ctx: SettingsContext): Promise<HostedSettings>;
render(ctx: RenderContext, params: AppParams): RenderResult | Promise<RenderResult>;
preview(ctx: PreviewContext): PreviewResult | Promise<PreviewResult>;
/** The app's check of a credential, unchecked; null for a bundle without one. */
verifyCredential(ctx: VerifyContext, credential: CredentialToVerify): Promise<unknown>;
}HostedSettings
What `settings` returned, and how long the platform may keep it. The fields come from app code, so the app server checks them before anything reads them.
interface HostedSettings {
readonly fields: unknown;
readonly ttlMs: number;
}LedApp
interface LedApp<Params = AppParams> {
/** Every setting the app has, as manifest fields (./settings.ts). */
settings(ctx: SettingsContext): Settings | Promise<Settings>;
/** How long the platform keeps what `settings` returned; a day unless the app says. */
readonly settingsTtlMs?: number | undefined;
render(ctx: RenderContext, params: Params): RenderResult | Promise<RenderResult>;
/** Must be deterministic and must not perform network requests. */
preview(ctx: PreviewContext): PreviewResult | Promise<PreviewResult>;
readonly verifyCredential?: CredentialCheck | undefined;
}Marquee
interface Marquee {
readonly frameCount: number;
readonly frameMs: number;
/** Scroll offset in px for a 0-based frame; last frame reaches distancePx. */
offsetAt(frame: number): number;
}MarqueeOptions
interface MarqueeOptions {
/** Total scroll distance in px (typically content width + viewport width). */
readonly distancePx: number;
readonly durationMs: number;
readonly fps?: number;
}PreviewContext
What a preview may read: its own published assets, and nothing that varies.
interface PreviewContext {
readonly assets: AppAssets;
}RenderContext
interface RenderContext {
readonly auth: AppAuth;
readonly credentials: AppCredentials;
readonly news: AppNews;
readonly locations: AppLocations;
readonly images: AppImages;
/** Published WebP assets of this bundle version. */
readonly assets: AppAssets;
/** Device model name (LD-Model). */
readonly model: string;
/** Screen resolution in pixels (LD-Resolution). */
readonly resolution: readonly [number, number];
/** Firmware version (LD-Firmware). */
readonly firmware: string;
/** Unique ID for this app instance (LD-Instance). */
readonly instanceId: string;
/** Previous cache ID for this app instance (LD-Cache-ID). */
readonly cacheId: string;
/** Language code (LD-Language). */
readonly language: string;
/** IANA TZ identifier (LD-Timezone). */
readonly timezone: string;
/** Display duration of this app in milliseconds (LD-Duration). */
readonly durationMs: number;
/**
* Server-assigned render instant. Apps must read wall-clock time from here
* (not Date.now()) so tests can pin it; preview() must not use time at all.
*/
readonly now: Date;
/**
* Read-through compute cache, keyed by the framework from the app's parsed
* params (all fields by default; narrow with `vary` to share). preview()
* has no cache — previews are deterministic and memoized by the server.
*/
readonly cache: AppCache;
/** Platform-provisioned secrets, restricted to the manifest declaration. */
readonly secrets: Readonly<Record<string, string>>;
}Rng
Seeded pseudo-random source for apps. Same seed, same sequence, on every runtime — a generated animation can be reproduced (and cache-addressed) by its seed alone instead of being stored.
interface Rng {
/** Uniform float in [0, 1). */
next(): number;
/** Uniform integer in [0, maxExclusive). */
int(maxExclusive: number): number;
/** New array with the items in shuffled order (Fisher-Yates). */
shuffle<T>(items: readonly T[]): T[];
/** `count` distinct items in draw order. */
sample<T>(items: readonly T[], count: number): T[];
}SettingsContext
What `settings` may read: nothing that belongs to one user or device.
interface SettingsContext {
readonly now: Date;
readonly cache: AppCache;
readonly secrets: Readonly<Record<string, string>>;
readonly assets: AppAssets;
}VerifyContext
What checking a credential may read; `fetch` reaches the hosts the app declares.
interface VerifyContext {
readonly now: Date;
readonly secrets: Readonly<Record<string, string>>;
}Types
AnimationBehavior
type AnimationBehavior = z.infer<typeof animationBehaviorSchema>;AppCategory
type AppCategory = z.infer<typeof appCategorySchema>;AppDefinition
An app as its code declares it: the render receives the parameters its settings make, and an app with a credential setting must check what users type into it.
type AppDefinition<S extends Settings> = AppBase<S> &
(HasCredential<S> extends true ? { readonly verifyCredential: CredentialCheck } : unknown);AppManifest
type AppManifest = Readonly<z.infer<typeof appManifestSchema>>;AppNetwork
type AppNetwork = z.infer<typeof networkSchema>;AppParams
type AppParams = z.infer<typeof appParamsSchema>;AppVisibility
type AppVisibility = z.infer<typeof appVisibilitySchema>;ConfigField
type ConfigField = Readonly<z.infer<typeof configFieldSchema>>;ConfigSource
Where raw (unparsed string) user config values come from. The default source reads the request query string, where devices send them.
type ConfigSource = (input: {
readonly request: Request;
readonly endpoint: string;
/** Context before platform capabilities and parsed params are available. */
readonly ctx: BaseContext;
}) => Record<string, string> | Promise<Record<string, string>>;ConfigValue
type ConfigValue = z.infer<typeof configValueSchema>;ConfigValueType
type ConfigValueType = z.infer<typeof configValueTypeSchema>;CredentialVerdict
type CredentialVerdict = Readonly<z.infer<typeof credentialVerdictSchema>>;DateValue
type DateValue = Readonly<z.infer<typeof dateValueSchema>>;FetchHandler
type FetchHandler = (request: Request) => Promise<Response>;Field
A manifest field, typed with the value a render receives for it.
type Field<Value> = ConfigField & { readonly __value?: Value };LocationValue
type LocationValue = Readonly<z.infer<typeof locationValueSchema>>;NewsResponse
type NewsResponse = z.infer<typeof newsResponseSchema>;ParamsOf
The parameters a render receives for these settings: unset is null unless it cannot be.
type ParamsOf<S extends Settings> = {
readonly [Key in keyof S]: S[Key] extends Field<infer Value>
? S[Key] extends { readonly required: true } | { readonly default: unknown }
? Value
: Value | null
: never;
};PreviewResult
type PreviewResult = Readonly<z.infer<typeof previewResultSchema>>;ProjectConfig
type ProjectConfig = z.infer<typeof projectSchema>;ProjectField
type ProjectField = z.infer<typeof fieldShape>;ProjectSettings
type ProjectSettings = z.infer<typeof settingsSchema>;PublicManifest
type PublicManifest = z.infer<typeof publicManifestSchema>;ReleaseManifest
type ReleaseManifest = z.infer<typeof releaseManifestSchema>;RenderResult
type RenderResult = Readonly<z.infer<typeof renderResultSchema>>;ResultImage
type ResultImage = z.infer<typeof resultImageSchema>;Settings
type Settings = Readonly<Record<string, Field<unknown>>>;TimeValue
type TimeValue = Readonly<z.infer<typeof timeValueSchema>>;