@linkworld_ai/sdk-browser
v0.8.0
Published
Browser-side bridge + lw-ui CSS framework for Linkworld partner-app bundles. 0.8.0: bridge.kv.list now clamps the platform's 1000-row cap client-side (no more 500'd routes), and the build emits inline source maps so devtools resolves stack traces without
Downloads
790
Maintainers
Readme
@linkworld_ai/sdk-browser
Browser-side bridge for partner-app bundles running inside the Linkworld tenant shell (Phase 3 M2b).
Why this exists
Your bundle JS runs in a sandboxed iframe on apps-cdn.linkworld.ai
with connect-src 'none' — it cannot fetch the Linkworld API
directly. The bridge wraps window.parent.postMessage so you can
make tool calls as if you were on the server side.
The parent (tenant shell on app.linkworld.ai) does the actual
HTTP request using the user's session cookie and forwards the result
back. Origin and install are verified on the parent before the call
ever leaves the user's browser.
Usage
import { LinkworldBridge, ToolCallError } from '@linkworld_ai/sdk-browser'
const bridge = new LinkworldBridge({
onInit: (info) => {
document.querySelector('#hello')!.textContent =
`Hi user ${info.userIdPrefix} on tenant ${info.tenantIdPrefix}`
},
// M3.12 — context bridge: react to platform-shell state.
onLanguageChange: (lang) => {
// bridge.language is also exposed as a getter
console.log('user is on', lang)
},
onRouteChange: (route) => {
// dim the bundle when the user navigates away from the app
document.body.dataset.parentRoute = route
},
})
document.querySelector('#send')!.addEventListener('click', async () => {
try {
const result = await bridge.tools.call('email_send', {
to: '[email protected]',
subject: 'Hello from the bundle',
body: 'This was sent via the bridge.',
})
console.log('sent', result)
} catch (err) {
if (err instanceof ToolCallError) {
if (err.decision === 'scope_denied') {
alert(`Missing scope: ${err.neededScopes?.join(', ')}`)
} else {
alert(err.message)
}
} else {
throw err
}
}
})
// Tell the parent to set the iframe height to fit content.
bridge.resize(document.documentElement.scrollHeight)
// Update the URL bar (deep-link friendly).
bridge.navigate('/settings')API
new LinkworldBridge(options?: BridgeOptions)
Wires the postMessage listener. One instance per page; constructing a second is allowed but unusual.
BridgeOptions:
parentOrigin?: string— defense-in-depth target origin for outbound posts. Default'*'(the parent's own origin gate is what enforces trust).toolCallTimeoutMs?: number— default 30000.onInit?: (info) => void— fires once when the parent posts theinitevent.onThemeChange?: (theme) => void— fires whenever the parent shell's color tokens change (light/dark toggle, design refresh).onLanguageChange?: (lang) => void— fires when the parent's BCP-47 language tag changes. (M3.12)onRouteChange?: (route) => void— fires when the parent shell's pathname changes (NOT when the bundle's own route changes). (M3.12)injectThemeStyle?: boolean— defaulttrue. Writes theme tokens as--lw-bg,--lw-surface, … on<html>.injectLanguage?: boolean— defaulttrue. Writes the language tag to<html lang>so:lang(de)selectors and screen-reader pronunciation just work. (M3.12)
bridge.tools.call<T>(tool, args?) => Promise<T>
Mirrors the server-side ctx.tools.call(...). Resolves with the
tool result; rejects with ToolCallError if the platform refused
(scope check, install missing, etc.) or with Error on timeout /
internal failure.
bridge.resize(height: number) => void
Asks the parent to set the iframe height. Useful for content-driven sizing.
bridge.navigate(path: string) => void
Asks the parent to update the URL bar to /apps/<slug>${path}.
Internal routing inside the bundle is up to you; this just keeps
the user's URL in sync.
bridge.init: BridgeInit | null
Last received init payload, or null if the parent hasn't sent
one yet. BridgeInit carries appId, tenantIdPrefix (8-char
display only — NOT for auth), userIdPrefix, theme, language
(BCP-47 tag), and routeName (parent pathname).
bridge.theme: BridgeTheme | null
Latest theme snapshot. colorScheme is 'light' | 'dark'; the rest
are CSS color strings (already painted as CSS custom properties on
<html> unless injectThemeStyle: false).
bridge.language: string | null (M3.12)
Latest BCP-47 language tag from the parent shell, e.g. 'en',
'de-DE'. null until the parent has pushed one. Already written
to document.documentElement.lang unless injectLanguage: false.
bridge.routeName: string | null (M3.12)
Parent shell's current pathname (NO scheme/host/query/hash). Useful for bundles that want to react to the user navigating away. NOT auth-scoped — any user reading this iframe sees their own pathname.
What you cannot do
- Read the user's cookie or localStorage of the tenant shell — the
iframe is cross-origin to
app.linkworld.ai. - Make outbound
fetch()calls from the bundle. CSPconnect-src 'none'blocks them. Usebridge.tools.call(...). - Call tools your app didn't declare in
manifest.tools. The platform rejects withdecision='scope_denied'.
