@bty/feed_app-runtime-sdk
v0.1.5
Published
Runtime SDK for feed-app template: auth / AI capabilities, multi-environment bridge (native App / iframe / web).
Keywords
Readme
@bty/feed_app-runtime-sdk
Runtime SDK for Feed-App pages.
It provides a small set of browser-safe APIs for reading host user context,
calling the Feed-App AI gateway, and reaching native device capabilities. The
package has no root entry; import from the capability sub-entries directly
(/user, /ai, /react, /device).
Install
pnpm add @bty/feed_app-runtime-sdkFailure conventions
Two failure styles, split by sub-entry — know which you're calling:
| Sub-entry | On failure | Why |
|---|---|---|
| /ai | throws a typed AiError subclass | callers must distinguish 401 / 402 / 429 / 4xx / 5xx to react correctly |
| /user | returns an empty value (never throws) | no host user context is a normal runtime state |
| /device | returns DeviceResult<T> (never throws) | denied / cancelled / unsupported native capability is normal control flow — branch on result.ok |
Empty value = "" for the one string API (getAuthTokenAsync), null
for getUserInfoAsync. Device APIs use one envelope instead:
{ ok: true, source, value } or
{ ok: false, source, reason, errorCode?, errorMessage? }.
Rule of thumb: wrap /ai calls in try/catch; branch on the return value for
/user; branch on result.ok for /device.
User
import {
getAuthTokenAsync,
getUserInfoAsync,
} from '@bty/feed_app-runtime-sdk/user'
const token = await getAuthTokenAsync()
const user = await getUserInfoAsync()getAuthTokenAsync() resolves to an auth token when the current host
environment can provide one. getUserInfoAsync() resolves to user information
or null.
Both APIs are safe to import during prerender or SSR. In a non-browser
environment they resolve to empty values instead of touching window.
AI
import { configureRuntime, openai } from '@bty/feed_app-runtime-sdk/ai'
configureRuntime({
apiBaseUrl: 'https://example.com',
})
const stream = await openai.chat.completions.create({
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'Hello' }],
stream: true,
})
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content
if (text) {
console.log(text)
}
}The AI entry supports:
- OpenAI-compatible chat completions
- Anthropic-compatible messages
- Image generation
- Text-to-speech
- Video generation
AbortSignalcancellation- Structured AI errors
When an AI HTTP request receives a non-2xx response, the SDK emits a sanitized
host notification before throwing the typed AiError. In the product page:
window.addEventListener('feed-app-runtime-sdk:ai-error', (event) => {
// event.detail: { status, code, message, path, method, ... }
})When the product runs inside a host iframe, the same payload is also sent to
window.parent.postMessage({ type: 'feed-app-runtime-sdk:ai-error', detail }, '*').
The payload intentionally omits raw provider bodies and headers; branch on
detail.status / detail.code for host-level UI.
Media payloads are pass-through.
images.generate/audio.speech.create/video.generations.createforward the body verbatim to the upstream provider — there is no client-side reshaping, and each model has its own shape. The param types only requiremodel; build the rest of the body from the model'sparameters.shapein the runtime model catalog, not from generic OpenAI SDK fields (quality: 'standard',style: 'vivid', … may be ignored or rejected by the target model).
import {
AuthRequiredError,
RateLimitError,
openai,
} from '@bty/feed_app-runtime-sdk/ai'
try {
await openai.chat.completions.create({
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'Hello' }],
})
} catch (error) {
if (error instanceof AuthRequiredError) {
// Ask the user to sign in.
} else if (error instanceof RateLimitError) {
// Show a rate limit message.
}
}React
import { useAuthToken, useUserInfo } from '@bty/feed_app-runtime-sdk/react'
export function App() {
const { token, loading: tokenLoading } = useAuthToken()
const { user } = useUserInfo()
return (
<main>
<p>{tokenLoading ? 'Loading...' : (user?.username ?? user?.nickname)}</p>
</main>
)
}For chat / image / TTS / video, call openai / anthropic from /ai directly
and wire your own loading / cancel / error state. Product-specific chat UX
varies enough that a generic hook tends to leak abstractions.
React is an optional peer dependency. Projects that do not import the /react
entry do not need to install React.
Device
Six host-bridged capabilities, each with a layered fallback: native App bridge
→ iframe parent relay → browser Web API → SDK failure. Import the device
namespace, or the individual capabilities for tree-shaking.
import {
haptics,
geolocation,
sensors,
camera,
files,
microphone,
} from '@bty/feed_app-runtime-sdk/device'
await haptics.impact('medium')
const pos = await geolocation.getCurrentPosition() // DeviceResult<PositionSample>
const motion = await sensors.watchMotion((s) => console.log(s.accelerationWithGravity))
const photo = await camera.capturePhoto({ camera: 'back' }) // DeviceResult<CapturedPhoto>
const picked = await files.pickFiles({ accept: ['image/*'], multiple: true })
const saved = await files.saveFile(blob, 'export.json') // DeviceResult<{ path?: string }>
const clip = await microphone.recordAudio({ maxDurationMs: 5000 }) // DeviceResult<AudioRecording>
if (pos.ok) console.log(pos.value.latitude, pos.value.longitude)
if (motion.ok) motion.value() // dispose the motion subscription
if (!saved.ok) console.warn(saved.reason, saved.errorCode, saved.errorMessage)Every device call returns DeviceResult<T> and never throws for normal runtime
states. On success, read result.value; on failure, branch on result.reason
("cancelled", "permission_denied", "not_supported", "timeout",
"invalid_response", or "failed"). source is "native", "iframe",
"web", or "sdk" and is useful for diagnostics.
pickFiles succeeds with { files: { file, path? }[] } (path present only on
the native bridge). watchPosition, watchMotion, and watchOrientation
succeed with a cleanup function in value. haptics.* and camera.stopStream
succeed with value: undefined.
For native/App saves, saveFile sends the Blob bytes through the bridge as
base64 with { filename, mime, size, base64 }. The host App must return an
ok: true envelope with result.saved === true; older metadata-only success
responses are ignored. USER_CANCELLED maps to reason: "cancelled"; native
failure responses keep errorCode / errorMessage on the returned
FileSaveResult. Top-level HostApp WebViews do not trust browser download
fallbacks after a native save failure, because WebView downloads can report
success without writing a file. Diagnostics are part of the normal saveFile
Result envelope.
Capability detection (synchronous, cheap — gate UI ahead of a call):
| Capability | Probe |
|---|---|
| Haptics | haptics.isSupported() |
| Geolocation | geolocation.isSupported() |
| Camera | camera.isSupported() |
| Sensors | sensors.isMotionSupported() / sensors.isOrientationSupported() |
| Files | files.isPickerSupported() (the modern picker; plain pick always works) |
| Microphone | microphone.isSupported() |
requestMotionPermission(), capturePhoto(), saveFile(), and pickFiles()
must be called from inside a user-gesture handler (tap / click) — browsers
silently deny these outside a gesture.
When a feed-app runs inside the host SPA iframe, one-shot device calls reuse the
same native command frame through window.parent.postMessage. The parent shell
forwards supported commands to the native App and returns the App envelope to
the child frame. If the parent cannot relay, SDK calls fall through to their web
fallback. camera.openStream() and microphone.startRecording() remain
web-only streaming APIs.
Published Files
The npm package publishes only built output and this README:
dist/**/*.jsdist/**/*.d.tsREADME.mdpackage.json
Source files and sourcemaps are not included in the npm tarball.
