@ateam-ai-mobile/plugin-runtime 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-P43IKNZE.mjs → chunk-56Z2X5ZB.mjs} +9 -1
- package/dist/chunk-56Z2X5ZB.mjs.map +1 -0
- package/dist/index.d.mts +68 -53
- package/dist/index.d.ts +68 -53
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-M6CS6D22.mjs → loader-3PQLHEEW.mjs} +2 -2
- package/package.json +2 -2
- package/src/api/plugins.ts +4 -0
- package/src/host.ts +22 -0
- package/src/index.ts +1 -0
- package/dist/chunk-P43IKNZE.mjs.map +0 -1
- /package/dist/{loader-M6CS6D22.mjs.map → loader-3PQLHEEW.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
registerService,
|
|
70
70
|
registerTools,
|
|
71
71
|
releaseDeviceStateHolds,
|
|
72
|
+
rendersNative,
|
|
72
73
|
resetDeviceToolLoader,
|
|
73
74
|
resolvePluginGrants,
|
|
74
75
|
resolvePluginRuntimeProps,
|
|
@@ -89,7 +90,7 @@ import {
|
|
|
89
90
|
useNativeBridge,
|
|
90
91
|
usePluginTheme,
|
|
91
92
|
useSurfaceStore
|
|
92
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-56Z2X5ZB.mjs";
|
|
93
94
|
|
|
94
95
|
// src/tools/platformTools.ts
|
|
95
96
|
var PLATFORM_PLUGIN_ID = "platform:device-tools";
|
|
@@ -132,10 +133,10 @@ var REAL_DEPS = {
|
|
|
132
133
|
// `./loader` reaches react-native via the bundle manager; import it only when
|
|
133
134
|
// an effect actually fires (real app), never at module load (tests).
|
|
134
135
|
resetLoader: () => {
|
|
135
|
-
void import("./loader-
|
|
136
|
+
void import("./loader-3PQLHEEW.mjs").then((m) => m.resetDeviceToolLoader());
|
|
136
137
|
},
|
|
137
138
|
load: () => {
|
|
138
|
-
void import("./loader-
|
|
139
|
+
void import("./loader-3PQLHEEW.mjs").then((m) => {
|
|
139
140
|
m.resetDeviceToolLoader();
|
|
140
141
|
return m.ensureDeviceToolsLoaded({ force: true });
|
|
141
142
|
});
|
|
@@ -462,6 +463,7 @@ export {
|
|
|
462
463
|
registerService,
|
|
463
464
|
registerTools,
|
|
464
465
|
releaseDeviceStateHolds,
|
|
466
|
+
rendersNative,
|
|
465
467
|
resetDeviceToolLoader,
|
|
466
468
|
resolvePluginGrants,
|
|
467
469
|
resolvePluginRuntimeProps,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/platformTools.ts","../src/tools/sessionSync.ts","../src/tools/serverCalls.ts","../src/hooks/usePlugin.ts","../src/hooks/usePluginRuntimeProps.ts","../src/hooks/useRemotePlugins.ts"],"sourcesContent":["/**\n * Platform device tools.\n *\n * These are not a solution's plugin tools — they ship with the SDK and every\n * host has them, which is what makes a device tool testable without deploying\n * anything to a tenant. Their names and contracts are declared once on the\n * server (connectors/platform/device-tools-mcp.json); this is the device half.\n *\n * Registered at import, so a client answers them even before any plugin bundle\n * has loaded — that is the point: proving the device path works should not\n * depend on the thing being proved.\n */\nimport { registerDeviceTools } from './registry';\n\nconst PLATFORM_PLUGIN_ID = 'platform:device-tools';\n\nexport function registerPlatformDeviceTools(): void {\n registerDeviceTools(PLATFORM_PLUGIN_ID, [\n {\n name: 'device.echo',\n description: \"Echo a string back from the user's own device.\",\n input_schema: {\n type: 'object',\n properties: { text: { type: 'string' } },\n required: ['text'],\n },\n run: (args: any) => ({\n ok: true,\n echoed: (args && args.text) || '',\n // The proof: a server-side implementation could not say this truthfully.\n ran_on: 'device',\n }),\n },\n {\n name: 'device.ping',\n description: 'Check that the device is reachable.',\n input_schema: { type: 'object', properties: {} },\n run: () => ({ ok: true, ran_on: 'device', at: new Date().toISOString() }),\n },\n ]);\n}\n","/**\n * Ambient device-tool registration, driven by the auth session (R14).\n *\n * The LOCAL reasoning path must be live BEFORE voice opens — otherwise the only\n * proof of a loadable external skill is a voice-triggered one. So the host wires\n * this to its EXISTING auth-session subscription (no new transport, no plugin\n * special-case): a tenant becoming authenticated loads that tenant's external\n * device-tool bundles (and re-ensures the static platform tools); a tenant\n * SWITCH is a change of tenant while still authenticated, so it clears the old\n * tenant's tools and reloads; sign-out resets the loader latch (the registry\n * itself is cleared by the host's logout teardown, which already calls\n * clearDeviceTools()).\n *\n * The decision is a PURE function so the lifecycle is testable without a network\n * or a device; the effects are injected (same DI shape as the host's logout\n * teardown) so a test drives them with fakes.\n */\nimport { registerPlatformDeviceTools } from './platformTools';\nimport { clearDeviceTools } from './registry';\n// NOTE: `./loader` pulls in the bundle manager (react-native, core), so it is\n// imported LAZILY inside the real effects below — keeping this module itself\n// node-testable (a test injects fake effects and never touches that graph).\n\nexport interface SessionSnapshot {\n authenticated: boolean;\n /** The tenant the session is scoped to (derive it; never a stored copy). */\n tenant: string | null;\n}\n\nexport type DeviceToolsAction = 'load' | 'reset' | 'none';\n\n/**\n * PURE: what should the device-tool registry do for this session transition?\n * - a new authenticated tenant (initial sign-in OR a switch) → 'load'\n * - the same authenticated tenant again (e.g. a silent token refresh) → 'none'\n * - leaving authenticated (sign-out) → 'reset'\n */\nexport function deviceToolsActionForSession(\n next: SessionSnapshot,\n prevTenant: string | null,\n): { action: DeviceToolsAction; tenant: string | null } {\n const tenant = next.authenticated ? (next.tenant ?? null) : null;\n if (tenant && tenant !== prevTenant) return { action: 'load', tenant };\n if (!tenant && prevTenant) return { action: 'reset', tenant: null };\n return { action: 'none', tenant: prevTenant };\n}\n\n/** The effects a session transition drives — injected so a test can fake them. */\nexport interface DeviceToolsSessionDeps {\n /** Drop the previous tenant's device tools (a switch does not run logout teardown). */\n clear: () => void;\n /** Ensure the static, host-owned platform device tools are present. */\n registerPlatform: () => void;\n /** Forget the \"already loaded\" latch so a later (non-forced) load runs — sign-out. */\n resetLoader: () => void;\n /** Fire-and-forget reload of the current tenant's external device-tool bundles. */\n load: () => void;\n}\n\nconst REAL_DEPS: DeviceToolsSessionDeps = {\n clear: clearDeviceTools,\n registerPlatform: registerPlatformDeviceTools,\n // `./loader` reaches react-native via the bundle manager; import it only when\n // an effect actually fires (real app), never at module load (tests).\n resetLoader: () => {\n void import('./loader').then((m) => m.resetDeviceToolLoader());\n },\n load: () => {\n void import('./loader').then((m) => {\n m.resetDeviceToolLoader();\n return m.ensureDeviceToolsLoaded({ force: true });\n });\n },\n};\n\nlet prevTenant: string | null = null;\n\n/**\n * Apply a session transition to the device-tool registry. Call from the host's\n * existing auth-session subscription with a derived snapshot. Idempotent per\n * tenant: repeated snapshots for the same authenticated tenant do nothing, so a\n * silent token refresh does not churn the registry. Returns the action taken.\n */\nexport function syncDeviceToolsForSession(\n next: SessionSnapshot,\n deps: DeviceToolsSessionDeps = REAL_DEPS,\n): DeviceToolsAction {\n const { action, tenant } = deviceToolsActionForSession(next, prevTenant);\n prevTenant = tenant;\n if (action === 'load') {\n // A new authenticated tenant: drop the prior tenant's tools, re-seat the\n // static platform tools, then reload this tenant's external bundles (forced).\n deps.clear();\n deps.registerPlatform();\n deps.load();\n } else if (action === 'reset') {\n // Sign-out: the host's logout teardown clears the registry; we only forget\n // the loader latch so a later sign-in reloads.\n deps.resetLoader();\n }\n return action;\n}\n\n/** Test seam: forget the last-seen tenant so a test starts from signed-out. */\nexport function __resetSessionSyncForTest(): void {\n prevTenant = null;\n}\n","/**\n * Device tools called BY THE SERVER.\n *\n * Two callers reach the same registered function on this phone:\n *\n * - the voice loop, which already runs here (no server hop at all), and\n * - the server reasoning loop, which cannot execute a device tool itself and\n * sends it down the client's stream instead.\n *\n * This is the second one. It rides the transport the `ui.*` plugin commands\n * already use — a command arrives on the job/actor stream, we run it, and post\n * the outcome back keyed by `correlationId`. Nothing new was invented; the\n * payload just carries a tool call instead of a UI command.\n *\n * The host wires this once at startup:\n *\n * pluginCommandEmitter.on('plugin-command', handleServerDeviceToolCall);\n */\nimport { callDeviceTool } from './registry';\nimport { ensureDeviceToolsLoaded } from './loader';\n\ntype PostResult = (correlationId: string, result: any, error?: string) => Promise<void>;\n\nlet postResult: PostResult | null = null;\n\n/**\n * Host DI: how to answer the server. `@ateam-ai-mobile/core` supplies\n * postCommandResult; kept injectable so this package stays free of the REST\n * client and so tests can observe the answer.\n */\nexport function setDeviceToolResultPoster(fn: PostResult): void {\n postResult = fn;\n}\n\n/** Is this stream command a device tool call? */\nexport function isDeviceToolCommand(cmd: any): boolean {\n return !!cmd && cmd.kind === 'device_tool' && typeof cmd.tool === 'string' && !!cmd.correlationId;\n}\n\n/**\n * Run a server-dispatched device tool and answer.\n *\n * Never throws — the server is blocked on the correlationId, so the one thing\n * we must not do is stay silent. Every path posts something back.\n */\nexport async function handleServerDeviceToolCall(cmd: any): Promise<void> {\n if (!isDeviceToolCommand(cmd)) return;\n\n const { tool, args, correlationId } = cmd;\n\n if (!postResult) {\n console.warn('[deviceTools] server called a tool but no result poster is installed');\n return;\n }\n\n try {\n // The server may reach us before anything opened a voice call, so the\n // plugins may not be loaded yet. Cheap and idempotent once warm.\n await ensureDeviceToolsLoaded();\n\n const outcome = await callDeviceTool(tool, args || {});\n console.log(`[deviceTools] server-called ${tool} → ${outcome.ok ? 'ok' : outcome.reason}`);\n\n await postResult(\n correlationId,\n outcome.ok ? { ok: true, ...unwrap(outcome.result) } : undefined,\n outcome.ok ? undefined : outcome.error,\n );\n } catch (err: any) {\n await postResult(correlationId, undefined, err?.message || String(err)).catch(() => {});\n }\n}\n\n/** Keep an object result flat for the reasoning loop; wrap anything else. */\nfunction unwrap(result: any): Record<string, any> {\n if (result && typeof result === 'object' && !Array.isArray(result)) return result;\n return { result };\n}\n","/**\n * usePlugin — single hook for loading any plugin.\n *\n * Simple async load → done pattern over the global BundleManager singleton\n * (deduplication + caching) and the host's native plugin registry.\n *\n * Usage:\n * const { plugin, loading, error } = usePlugin(pluginId, manifest);\n * if (loading) return <Spinner />;\n * if (error) return <Error message={error} />;\n * return <plugin.Component bridge={bridge} device={device} theme={theme} context={context} />;\n */\nimport { useState, useEffect } from 'react';\nimport { bundleManager } from '../bundles/bundleManager';\nimport { nativePluginRegistry } from '../host';\nimport { resolvePluginGrants } from '../pluginRuntime';\nimport type { NativeCapabilities, RegisteredUIPlugin } from '../sdk/types';\nimport type { DeviceBridgeDomain } from '../sdk/device-state';\nimport { pluginBundleUrl } from '../bundles/bundleUrl';\n\ninterface PluginManifestLike {\n version?: string;\n name?: string;\n render?: any;\n _connectorId?: string;\n capabilities?: Record<string, unknown>;\n native?: NativeCapabilities;\n stateDomains?: DeviceBridgeDomain[];\n}\n\ninterface UsePluginResult {\n plugin: RegisteredUIPlugin | null;\n loading: boolean;\n error: string | null;\n bundleMissing: boolean;\n}\n\n/**\n * Compute the bundle URL.\n *\n * **Policy:** bundle URLs are ALWAYS derived from the pluginId via the\n * canonical platform route /api/ui-plugins/{pluginId}/bundle.js.\n * Connector-supplied bundleUrl fields are IGNORED — the Builder strips them\n * at introspection time, but we treat any survivors as bugs and ignore them\n * here too (defense in depth vs \"hand-coded URL with bad encoding causes\n * silent iframe fallback\" bugs).\n */\ntype BundleUrlSource = 'derived-default' | 'ignored-explicit' | 'none';\nfunction extractBundleUrl(pluginId: string, manifest: PluginManifestLike): { url: string | null; source: BundleUrlSource; ignored?: string } {\n const render = manifest.render;\n if (!render || typeof render !== 'object') return { url: null, source: 'none' };\n\n // Detect (and refuse) any explicit bundle URL the connector tried to inject.\n const injected =\n (manifest as any).bundleUrl ||\n (render as any).bundleUrl ||\n render.reactNative?.bundleUrl ||\n null;\n\n // Mobile decides native vs web INDEPENDENT of render.mode. The server's\n // cp.getContextPlugin flattens mode to \"iframe\" whenever an iframeUrl is\n // available (for web-frontend compatibility) — even when a reactNative\n // component is also declared. So we derive the bundle URL whenever a\n // reactNative component is present, regardless of what `mode` says.\n const hasNativeComponent = !!(render.reactNative?.component || render.component);\n if (hasNativeComponent) {\n return {\n url: pluginBundleUrl(pluginId),\n source: injected ? 'ignored-explicit' : 'derived-default',\n ...(injected && { ignored: injected }),\n };\n }\n\n return { url: null, source: 'none' };\n}\n\n/**\n * Extract the SHA-256 integrity hash from the manifest (server-provided).\n * BundleManager refuses to evaluate a downloaded bundle whose bytes don't\n * hash to this value — defense vs MITM/CDN tampering.\n */\nfunction extractBundleHash(manifest: PluginManifestLike): string | undefined {\n if ((manifest as any).bundleHash) return (manifest as any).bundleHash;\n const render = manifest.render;\n if (!render || typeof render !== 'object') return undefined;\n if (render.bundleHash) return render.bundleHash;\n if (render.reactNative?.bundleHash) return render.reactNative.bundleHash;\n return undefined;\n}\n\n/**\n * Extract component name from manifest render field.\n */\nfunction extractComponentId(manifest: PluginManifestLike): string | null {\n const render = manifest.render;\n if (!render || typeof render !== 'object') return null;\n return render.component || render.reactNative?.component || null;\n}\n\nexport function usePlugin(pluginId: string, manifest: PluginManifestLike): UsePluginResult {\n const [plugin, setPlugin] = useState<RegisteredUIPlugin | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [bundleMissing, setBundleMissing] = useState(false);\n\n const version = manifest.version || '1.0.0';\n const { url: bundleUrl, ignored: ignoredUrl } = extractBundleUrl(pluginId, manifest);\n const bundleHash = extractBundleHash(manifest);\n const componentId = extractComponentId(manifest);\n const lookupId = componentId || pluginId;\n // The manifest's grants — the authority for the bundle's device tools, the\n // same one the renderer gates the component with. Keyed by value so a fresh\n // manifest object with the same grants does not reload the plugin.\n const grants = resolvePluginGrants({ pluginId, manifest });\n const grantsKey = JSON.stringify(grants);\n\n if (ignoredUrl) {\n console.warn(`[usePlugin] ${pluginId}: IGNORED connector-injected bundleUrl=\"${ignoredUrl}\". The host derives bundle URLs from pluginId via the canonical route.`);\n }\n\n useEffect(() => {\n let cancelled = false;\n\n (async () => {\n try {\n // Path 1: Local registry (host-bundled / dev plugins)\n if (nativePluginRegistry.has(lookupId)) {\n const registered = await nativePluginRegistry.get(lookupId);\n if (cancelled) return;\n\n if (!registered) {\n throw new Error(`Local plugin not found: ${lookupId}`);\n }\n if (registered.type === 'service') {\n throw new Error(`Plugin ${lookupId} is a service plugin and cannot be rendered`);\n }\n\n setPlugin(registered as RegisteredUIPlugin);\n setLoading(false);\n return;\n }\n\n // Path 2: Remote bundle (via global BundleManager)\n if (bundleUrl) {\n try {\n const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash, grants);\n if (cancelled) return;\n setPlugin(component);\n setLoading(false);\n return;\n } catch (bundleErr) {\n const msg = bundleErr instanceof Error ? bundleErr.message : String(bundleErr);\n // warn (not error) — this is a handled path: we fall back to WebView.\n console.warn(`[usePlugin] ${pluginId}: Bundle load failed, falling back to WebView:`, msg);\n // RN bundle failed — connector declared a component but didn't\n // ship a usable bundle (404, eval error, integrity fail, etc).\n // Mark \"bundle missing\" so the parent renderer can fall back to\n // the WebView/iframe when the manifest also has an iframeUrl.\n if (!cancelled) {\n setBundleMissing(true);\n setLoading(false);\n }\n return;\n }\n }\n\n // No native source available. If the manifest also has an iframeUrl,\n // mark \"bundle missing\" so the parent falls back to the WebView.\n // Only throw when there's truly nothing renderable.\n const iframeUrl = manifest?.render?.iframeUrl\n || (manifest?.render?.mode === 'adaptive' && manifest?.render?.iframe?.iframeUrl);\n if (iframeUrl) {\n if (!cancelled) {\n setBundleMissing(true);\n setLoading(false);\n }\n return;\n }\n throw new Error(\n `Plugin \"${manifest.name || pluginId}\" has no bundle URL and is not in the local registry. ` +\n `Check that the solution definition includes a valid render.reactNative.bundleUrl.`\n );\n } catch (err) {\n if (cancelled) return;\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`[usePlugin] Failed to load ${pluginId}:`, msg);\n setError(msg);\n setLoading(false);\n }\n })();\n\n return () => { cancelled = true; };\n }, [pluginId, lookupId, bundleUrl, bundleHash, version, grantsKey]);\n\n return { plugin, loading, error, bundleMissing };\n}\n","// usePluginRuntimeProps — the gated { device, native, deviceState, voice } for ONE\n// mounted surface, with the device-state sessions it starts released when it\n// unmounts (AI Models F2: a screen that closes leaves no camera running).\n//\n// Every surface that renders a plugin uses this instead of calling\n// resolvePluginRuntimeProps in a useMemo, so the release cannot be forgotten.\n// Each mount is its own holder: a drawer and an embedded view of one plugin hold\n// the same camera separately, and it ends only when both have closed. A mount\n// that switches plugin releases the old plugin's sessions, and one whose grants\n// shrink releases the domains it lost.\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport { resolvePluginGrants, resolvePluginRuntimeProps, type PluginRuntimeSource } from '../pluginRuntime';\nimport { releaseDeviceStateHolds } from '../deviceStateHost';\nimport type { PluginRuntimeProps } from '../pluginRuntime';\n\nlet mounts = 0;\n\nexport function usePluginRuntimeProps(src: PluginRuntimeSource, deps: readonly unknown[]): PluginRuntimeProps {\n // One holder per plugin on this mount: a renderer that switches to ANOTHER plugin\n // gets a new holder, and the effect below releases what the old one held.\n const holderRef = useRef<{ pluginId: string; holder: string } | null>(null);\n if (holderRef.current?.pluginId !== src.pluginId) {\n holderRef.current = { pluginId: src.pluginId, holder: `${src.pluginId}#${++mounts}` };\n }\n const holder = holderRef.current.holder;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const props = useMemo(() => resolvePluginRuntimeProps(src, holder), [...deps, holder]);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const granted = useMemo(() => resolvePluginGrants(src).stateDomains ?? [], deps);\n\n // Unmount, or a switch to another plugin: release everything this holder holds.\n useEffect(() => () => { void releaseDeviceStateHolds(holder); }, [holder]);\n\n // Grants changed while mounted: a domain no longer granted is released now. The\n // new facade refuses it, so the plugin could not stop that session itself.\n const grantedKey = granted.join(',');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(() => { void releaseDeviceStateHolds(holder, granted); }, [holder, grantedKey]);\n\n return props;\n}\n","import { useEffect, useState, useCallback } from 'react';\nimport { apiFetch } from '@ateam-ai-mobile/core';\nimport type { PluginManifest } from '../sdk/types';\n\nexport interface RemotePlugin {\n id: string;\n name: string;\n version: string;\n manifest: PluginManifest;\n source: 'remote' | 'local'; // Where this plugin came from\n}\n\ninterface UseRemotePluginsResult {\n plugins: RemotePlugin[];\n loading: boolean;\n error: string | null;\n refreshing: boolean;\n refresh: () => Promise<void>;\n}\n\n/**\n * Fetches available UI plugins from A-Team Core for the current solution.\n * Uses apiFetch which auto-injects JWT + tenant headers.\n */\nexport function useRemotePlugins(solutionId?: string): UseRemotePluginsResult {\n const [plugins, setPlugins] = useState<RemotePlugin[]>([]);\n const [loading, setLoading] = useState(true);\n const [refreshing, setRefreshing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const fetchRemotePlugins = useCallback(async () => {\n try {\n setError(null);\n\n const targetSolutionId = solutionId || 'current';\n\n let data: any;\n try {\n data = await apiFetch<any>(`/api/solutions/${targetSolutionId}/ui-plugins`);\n } catch (err: any) {\n if (err?.message?.includes('404')) {\n console.warn(`[useRemotePlugins] Solution ${targetSolutionId} not found or has no plugins`);\n setPlugins([]);\n return;\n }\n throw err;\n }\n const uiPlugins: RemotePlugin[] = (data.plugins || []).map((plugin: any) => ({\n id: plugin.id,\n name: plugin.name,\n version: plugin.version || '1.0.0',\n source: 'remote' as const,\n manifest: {\n id: plugin.id,\n name: plugin.name,\n version: plugin.version || '1.0.0',\n type: 'ui',\n description: plugin.description || '',\n capabilities: plugin.capabilities || {},\n channels: plugin.channels || ['default'],\n render: (plugin.render && typeof plugin.render === 'object') ? plugin.render : { mode: 'react-native' as const, component: plugin.name || plugin.id },\n entry_point: plugin.entry_point || plugin.name,\n sandbox: plugin.sandbox !== false,\n requires_auth: plugin.requires_auth !== false,\n } as PluginManifest,\n }));\n\n setPlugins(uiPlugins);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[useRemotePlugins] Failed to fetch plugins:', message);\n setError(message);\n setPlugins([]); // Clear plugins on error\n } finally {\n setLoading(false);\n setRefreshing(false);\n }\n }, [solutionId]);\n\n const refresh = useCallback(async () => {\n setRefreshing(true);\n await fetchRemotePlugins();\n }, [fetchRemotePlugins]);\n\n useEffect(() => {\n setLoading(true);\n fetchRemotePlugins();\n }, [fetchRemotePlugins]);\n\n return {\n plugins,\n loading,\n error,\n refreshing,\n refresh,\n };\n}\n"],"mappings":"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","names":["prevTenant","useEffect","useEffect","useEffect","useState"]}
|
|
1
|
+
{"version":3,"sources":["../src/tools/platformTools.ts","../src/tools/sessionSync.ts","../src/tools/serverCalls.ts","../src/hooks/usePlugin.ts","../src/hooks/usePluginRuntimeProps.ts","../src/hooks/useRemotePlugins.ts"],"sourcesContent":["/**\n * Platform device tools.\n *\n * These are not a solution's plugin tools — they ship with the SDK and every\n * host has them, which is what makes a device tool testable without deploying\n * anything to a tenant. Their names and contracts are declared once on the\n * server (connectors/platform/device-tools-mcp.json); this is the device half.\n *\n * Registered at import, so a client answers them even before any plugin bundle\n * has loaded — that is the point: proving the device path works should not\n * depend on the thing being proved.\n */\nimport { registerDeviceTools } from './registry';\n\nconst PLATFORM_PLUGIN_ID = 'platform:device-tools';\n\nexport function registerPlatformDeviceTools(): void {\n registerDeviceTools(PLATFORM_PLUGIN_ID, [\n {\n name: 'device.echo',\n description: \"Echo a string back from the user's own device.\",\n input_schema: {\n type: 'object',\n properties: { text: { type: 'string' } },\n required: ['text'],\n },\n run: (args: any) => ({\n ok: true,\n echoed: (args && args.text) || '',\n // The proof: a server-side implementation could not say this truthfully.\n ran_on: 'device',\n }),\n },\n {\n name: 'device.ping',\n description: 'Check that the device is reachable.',\n input_schema: { type: 'object', properties: {} },\n run: () => ({ ok: true, ran_on: 'device', at: new Date().toISOString() }),\n },\n ]);\n}\n","/**\n * Ambient device-tool registration, driven by the auth session (R14).\n *\n * The LOCAL reasoning path must be live BEFORE voice opens — otherwise the only\n * proof of a loadable external skill is a voice-triggered one. So the host wires\n * this to its EXISTING auth-session subscription (no new transport, no plugin\n * special-case): a tenant becoming authenticated loads that tenant's external\n * device-tool bundles (and re-ensures the static platform tools); a tenant\n * SWITCH is a change of tenant while still authenticated, so it clears the old\n * tenant's tools and reloads; sign-out resets the loader latch (the registry\n * itself is cleared by the host's logout teardown, which already calls\n * clearDeviceTools()).\n *\n * The decision is a PURE function so the lifecycle is testable without a network\n * or a device; the effects are injected (same DI shape as the host's logout\n * teardown) so a test drives them with fakes.\n */\nimport { registerPlatformDeviceTools } from './platformTools';\nimport { clearDeviceTools } from './registry';\n// NOTE: `./loader` pulls in the bundle manager (react-native, core), so it is\n// imported LAZILY inside the real effects below — keeping this module itself\n// node-testable (a test injects fake effects and never touches that graph).\n\nexport interface SessionSnapshot {\n authenticated: boolean;\n /** The tenant the session is scoped to (derive it; never a stored copy). */\n tenant: string | null;\n}\n\nexport type DeviceToolsAction = 'load' | 'reset' | 'none';\n\n/**\n * PURE: what should the device-tool registry do for this session transition?\n * - a new authenticated tenant (initial sign-in OR a switch) → 'load'\n * - the same authenticated tenant again (e.g. a silent token refresh) → 'none'\n * - leaving authenticated (sign-out) → 'reset'\n */\nexport function deviceToolsActionForSession(\n next: SessionSnapshot,\n prevTenant: string | null,\n): { action: DeviceToolsAction; tenant: string | null } {\n const tenant = next.authenticated ? (next.tenant ?? null) : null;\n if (tenant && tenant !== prevTenant) return { action: 'load', tenant };\n if (!tenant && prevTenant) return { action: 'reset', tenant: null };\n return { action: 'none', tenant: prevTenant };\n}\n\n/** The effects a session transition drives — injected so a test can fake them. */\nexport interface DeviceToolsSessionDeps {\n /** Drop the previous tenant's device tools (a switch does not run logout teardown). */\n clear: () => void;\n /** Ensure the static, host-owned platform device tools are present. */\n registerPlatform: () => void;\n /** Forget the \"already loaded\" latch so a later (non-forced) load runs — sign-out. */\n resetLoader: () => void;\n /** Fire-and-forget reload of the current tenant's external device-tool bundles. */\n load: () => void;\n}\n\nconst REAL_DEPS: DeviceToolsSessionDeps = {\n clear: clearDeviceTools,\n registerPlatform: registerPlatformDeviceTools,\n // `./loader` reaches react-native via the bundle manager; import it only when\n // an effect actually fires (real app), never at module load (tests).\n resetLoader: () => {\n void import('./loader').then((m) => m.resetDeviceToolLoader());\n },\n load: () => {\n void import('./loader').then((m) => {\n m.resetDeviceToolLoader();\n return m.ensureDeviceToolsLoaded({ force: true });\n });\n },\n};\n\nlet prevTenant: string | null = null;\n\n/**\n * Apply a session transition to the device-tool registry. Call from the host's\n * existing auth-session subscription with a derived snapshot. Idempotent per\n * tenant: repeated snapshots for the same authenticated tenant do nothing, so a\n * silent token refresh does not churn the registry. Returns the action taken.\n */\nexport function syncDeviceToolsForSession(\n next: SessionSnapshot,\n deps: DeviceToolsSessionDeps = REAL_DEPS,\n): DeviceToolsAction {\n const { action, tenant } = deviceToolsActionForSession(next, prevTenant);\n prevTenant = tenant;\n if (action === 'load') {\n // A new authenticated tenant: drop the prior tenant's tools, re-seat the\n // static platform tools, then reload this tenant's external bundles (forced).\n deps.clear();\n deps.registerPlatform();\n deps.load();\n } else if (action === 'reset') {\n // Sign-out: the host's logout teardown clears the registry; we only forget\n // the loader latch so a later sign-in reloads.\n deps.resetLoader();\n }\n return action;\n}\n\n/** Test seam: forget the last-seen tenant so a test starts from signed-out. */\nexport function __resetSessionSyncForTest(): void {\n prevTenant = null;\n}\n","/**\n * Device tools called BY THE SERVER.\n *\n * Two callers reach the same registered function on this phone:\n *\n * - the voice loop, which already runs here (no server hop at all), and\n * - the server reasoning loop, which cannot execute a device tool itself and\n * sends it down the client's stream instead.\n *\n * This is the second one. It rides the transport the `ui.*` plugin commands\n * already use — a command arrives on the job/actor stream, we run it, and post\n * the outcome back keyed by `correlationId`. Nothing new was invented; the\n * payload just carries a tool call instead of a UI command.\n *\n * The host wires this once at startup:\n *\n * pluginCommandEmitter.on('plugin-command', handleServerDeviceToolCall);\n */\nimport { callDeviceTool } from './registry';\nimport { ensureDeviceToolsLoaded } from './loader';\n\ntype PostResult = (correlationId: string, result: any, error?: string) => Promise<void>;\n\nlet postResult: PostResult | null = null;\n\n/**\n * Host DI: how to answer the server. `@ateam-ai-mobile/core` supplies\n * postCommandResult; kept injectable so this package stays free of the REST\n * client and so tests can observe the answer.\n */\nexport function setDeviceToolResultPoster(fn: PostResult): void {\n postResult = fn;\n}\n\n/** Is this stream command a device tool call? */\nexport function isDeviceToolCommand(cmd: any): boolean {\n return !!cmd && cmd.kind === 'device_tool' && typeof cmd.tool === 'string' && !!cmd.correlationId;\n}\n\n/**\n * Run a server-dispatched device tool and answer.\n *\n * Never throws — the server is blocked on the correlationId, so the one thing\n * we must not do is stay silent. Every path posts something back.\n */\nexport async function handleServerDeviceToolCall(cmd: any): Promise<void> {\n if (!isDeviceToolCommand(cmd)) return;\n\n const { tool, args, correlationId } = cmd;\n\n if (!postResult) {\n console.warn('[deviceTools] server called a tool but no result poster is installed');\n return;\n }\n\n try {\n // The server may reach us before anything opened a voice call, so the\n // plugins may not be loaded yet. Cheap and idempotent once warm.\n await ensureDeviceToolsLoaded();\n\n const outcome = await callDeviceTool(tool, args || {});\n console.log(`[deviceTools] server-called ${tool} → ${outcome.ok ? 'ok' : outcome.reason}`);\n\n await postResult(\n correlationId,\n outcome.ok ? { ok: true, ...unwrap(outcome.result) } : undefined,\n outcome.ok ? undefined : outcome.error,\n );\n } catch (err: any) {\n await postResult(correlationId, undefined, err?.message || String(err)).catch(() => {});\n }\n}\n\n/** Keep an object result flat for the reasoning loop; wrap anything else. */\nfunction unwrap(result: any): Record<string, any> {\n if (result && typeof result === 'object' && !Array.isArray(result)) return result;\n return { result };\n}\n","/**\n * usePlugin — single hook for loading any plugin.\n *\n * Simple async load → done pattern over the global BundleManager singleton\n * (deduplication + caching) and the host's native plugin registry.\n *\n * Usage:\n * const { plugin, loading, error } = usePlugin(pluginId, manifest);\n * if (loading) return <Spinner />;\n * if (error) return <Error message={error} />;\n * return <plugin.Component bridge={bridge} device={device} theme={theme} context={context} />;\n */\nimport { useState, useEffect } from 'react';\nimport { bundleManager } from '../bundles/bundleManager';\nimport { nativePluginRegistry } from '../host';\nimport { resolvePluginGrants } from '../pluginRuntime';\nimport type { NativeCapabilities, RegisteredUIPlugin } from '../sdk/types';\nimport type { DeviceBridgeDomain } from '../sdk/device-state';\nimport { pluginBundleUrl } from '../bundles/bundleUrl';\n\ninterface PluginManifestLike {\n version?: string;\n name?: string;\n render?: any;\n _connectorId?: string;\n capabilities?: Record<string, unknown>;\n native?: NativeCapabilities;\n stateDomains?: DeviceBridgeDomain[];\n}\n\ninterface UsePluginResult {\n plugin: RegisteredUIPlugin | null;\n loading: boolean;\n error: string | null;\n bundleMissing: boolean;\n}\n\n/**\n * Compute the bundle URL.\n *\n * **Policy:** bundle URLs are ALWAYS derived from the pluginId via the\n * canonical platform route /api/ui-plugins/{pluginId}/bundle.js.\n * Connector-supplied bundleUrl fields are IGNORED — the Builder strips them\n * at introspection time, but we treat any survivors as bugs and ignore them\n * here too (defense in depth vs \"hand-coded URL with bad encoding causes\n * silent iframe fallback\" bugs).\n */\ntype BundleUrlSource = 'derived-default' | 'ignored-explicit' | 'none';\nfunction extractBundleUrl(pluginId: string, manifest: PluginManifestLike): { url: string | null; source: BundleUrlSource; ignored?: string } {\n const render = manifest.render;\n if (!render || typeof render !== 'object') return { url: null, source: 'none' };\n\n // Detect (and refuse) any explicit bundle URL the connector tried to inject.\n const injected =\n (manifest as any).bundleUrl ||\n (render as any).bundleUrl ||\n render.reactNative?.bundleUrl ||\n null;\n\n // Mobile decides native vs web INDEPENDENT of render.mode. The server's\n // cp.getContextPlugin flattens mode to \"iframe\" whenever an iframeUrl is\n // available (for web-frontend compatibility) — even when a reactNative\n // component is also declared. So we derive the bundle URL whenever a\n // reactNative component is present, regardless of what `mode` says.\n const hasNativeComponent = !!(render.reactNative?.component || render.component);\n if (hasNativeComponent) {\n return {\n url: pluginBundleUrl(pluginId),\n source: injected ? 'ignored-explicit' : 'derived-default',\n ...(injected && { ignored: injected }),\n };\n }\n\n return { url: null, source: 'none' };\n}\n\n/**\n * Extract the SHA-256 integrity hash from the manifest (server-provided).\n * BundleManager refuses to evaluate a downloaded bundle whose bytes don't\n * hash to this value — defense vs MITM/CDN tampering.\n */\nfunction extractBundleHash(manifest: PluginManifestLike): string | undefined {\n if ((manifest as any).bundleHash) return (manifest as any).bundleHash;\n const render = manifest.render;\n if (!render || typeof render !== 'object') return undefined;\n if (render.bundleHash) return render.bundleHash;\n if (render.reactNative?.bundleHash) return render.reactNative.bundleHash;\n return undefined;\n}\n\n/**\n * Extract component name from manifest render field.\n */\nfunction extractComponentId(manifest: PluginManifestLike): string | null {\n const render = manifest.render;\n if (!render || typeof render !== 'object') return null;\n return render.component || render.reactNative?.component || null;\n}\n\nexport function usePlugin(pluginId: string, manifest: PluginManifestLike): UsePluginResult {\n const [plugin, setPlugin] = useState<RegisteredUIPlugin | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [bundleMissing, setBundleMissing] = useState(false);\n\n const version = manifest.version || '1.0.0';\n const { url: bundleUrl, ignored: ignoredUrl } = extractBundleUrl(pluginId, manifest);\n const bundleHash = extractBundleHash(manifest);\n const componentId = extractComponentId(manifest);\n const lookupId = componentId || pluginId;\n // The manifest's grants — the authority for the bundle's device tools, the\n // same one the renderer gates the component with. Keyed by value so a fresh\n // manifest object with the same grants does not reload the plugin.\n const grants = resolvePluginGrants({ pluginId, manifest });\n const grantsKey = JSON.stringify(grants);\n\n if (ignoredUrl) {\n console.warn(`[usePlugin] ${pluginId}: IGNORED connector-injected bundleUrl=\"${ignoredUrl}\". The host derives bundle URLs from pluginId via the canonical route.`);\n }\n\n useEffect(() => {\n let cancelled = false;\n\n (async () => {\n try {\n // Path 1: Local registry (host-bundled / dev plugins)\n if (nativePluginRegistry.has(lookupId)) {\n const registered = await nativePluginRegistry.get(lookupId);\n if (cancelled) return;\n\n if (!registered) {\n throw new Error(`Local plugin not found: ${lookupId}`);\n }\n if (registered.type === 'service') {\n throw new Error(`Plugin ${lookupId} is a service plugin and cannot be rendered`);\n }\n\n setPlugin(registered as RegisteredUIPlugin);\n setLoading(false);\n return;\n }\n\n // Path 2: Remote bundle (via global BundleManager)\n if (bundleUrl) {\n try {\n const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash, grants);\n if (cancelled) return;\n setPlugin(component);\n setLoading(false);\n return;\n } catch (bundleErr) {\n const msg = bundleErr instanceof Error ? bundleErr.message : String(bundleErr);\n // warn (not error) — this is a handled path: we fall back to WebView.\n console.warn(`[usePlugin] ${pluginId}: Bundle load failed, falling back to WebView:`, msg);\n // RN bundle failed — connector declared a component but didn't\n // ship a usable bundle (404, eval error, integrity fail, etc).\n // Mark \"bundle missing\" so the parent renderer can fall back to\n // the WebView/iframe when the manifest also has an iframeUrl.\n if (!cancelled) {\n setBundleMissing(true);\n setLoading(false);\n }\n return;\n }\n }\n\n // No native source available. If the manifest also has an iframeUrl,\n // mark \"bundle missing\" so the parent falls back to the WebView.\n // Only throw when there's truly nothing renderable.\n const iframeUrl = manifest?.render?.iframeUrl\n || (manifest?.render?.mode === 'adaptive' && manifest?.render?.iframe?.iframeUrl);\n if (iframeUrl) {\n if (!cancelled) {\n setBundleMissing(true);\n setLoading(false);\n }\n return;\n }\n throw new Error(\n `Plugin \"${manifest.name || pluginId}\" has no bundle URL and is not in the local registry. ` +\n `Check that the solution definition includes a valid render.reactNative.bundleUrl.`\n );\n } catch (err) {\n if (cancelled) return;\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`[usePlugin] Failed to load ${pluginId}:`, msg);\n setError(msg);\n setLoading(false);\n }\n })();\n\n return () => { cancelled = true; };\n }, [pluginId, lookupId, bundleUrl, bundleHash, version, grantsKey]);\n\n return { plugin, loading, error, bundleMissing };\n}\n","// usePluginRuntimeProps — the gated { device, native, deviceState, voice } for ONE\n// mounted surface, with the device-state sessions it starts released when it\n// unmounts (AI Models F2: a screen that closes leaves no camera running).\n//\n// Every surface that renders a plugin uses this instead of calling\n// resolvePluginRuntimeProps in a useMemo, so the release cannot be forgotten.\n// Each mount is its own holder: a drawer and an embedded view of one plugin hold\n// the same camera separately, and it ends only when both have closed. A mount\n// that switches plugin releases the old plugin's sessions, and one whose grants\n// shrink releases the domains it lost.\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport { resolvePluginGrants, resolvePluginRuntimeProps, type PluginRuntimeSource } from '../pluginRuntime';\nimport { releaseDeviceStateHolds } from '../deviceStateHost';\nimport type { PluginRuntimeProps } from '../pluginRuntime';\n\nlet mounts = 0;\n\nexport function usePluginRuntimeProps(src: PluginRuntimeSource, deps: readonly unknown[]): PluginRuntimeProps {\n // One holder per plugin on this mount: a renderer that switches to ANOTHER plugin\n // gets a new holder, and the effect below releases what the old one held.\n const holderRef = useRef<{ pluginId: string; holder: string } | null>(null);\n if (holderRef.current?.pluginId !== src.pluginId) {\n holderRef.current = { pluginId: src.pluginId, holder: `${src.pluginId}#${++mounts}` };\n }\n const holder = holderRef.current.holder;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const props = useMemo(() => resolvePluginRuntimeProps(src, holder), [...deps, holder]);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const granted = useMemo(() => resolvePluginGrants(src).stateDomains ?? [], deps);\n\n // Unmount, or a switch to another plugin: release everything this holder holds.\n useEffect(() => () => { void releaseDeviceStateHolds(holder); }, [holder]);\n\n // Grants changed while mounted: a domain no longer granted is released now. The\n // new facade refuses it, so the plugin could not stop that session itself.\n const grantedKey = granted.join(',');\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(() => { void releaseDeviceStateHolds(holder, granted); }, [holder, grantedKey]);\n\n return props;\n}\n","import { useEffect, useState, useCallback } from 'react';\nimport { apiFetch } from '@ateam-ai-mobile/core';\nimport type { PluginManifest } from '../sdk/types';\n\nexport interface RemotePlugin {\n id: string;\n name: string;\n version: string;\n manifest: PluginManifest;\n source: 'remote' | 'local'; // Where this plugin came from\n}\n\ninterface UseRemotePluginsResult {\n plugins: RemotePlugin[];\n loading: boolean;\n error: string | null;\n refreshing: boolean;\n refresh: () => Promise<void>;\n}\n\n/**\n * Fetches available UI plugins from A-Team Core for the current solution.\n * Uses apiFetch which auto-injects JWT + tenant headers.\n */\nexport function useRemotePlugins(solutionId?: string): UseRemotePluginsResult {\n const [plugins, setPlugins] = useState<RemotePlugin[]>([]);\n const [loading, setLoading] = useState(true);\n const [refreshing, setRefreshing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const fetchRemotePlugins = useCallback(async () => {\n try {\n setError(null);\n\n const targetSolutionId = solutionId || 'current';\n\n let data: any;\n try {\n data = await apiFetch<any>(`/api/solutions/${targetSolutionId}/ui-plugins`);\n } catch (err: any) {\n if (err?.message?.includes('404')) {\n console.warn(`[useRemotePlugins] Solution ${targetSolutionId} not found or has no plugins`);\n setPlugins([]);\n return;\n }\n throw err;\n }\n const uiPlugins: RemotePlugin[] = (data.plugins || []).map((plugin: any) => ({\n id: plugin.id,\n name: plugin.name,\n version: plugin.version || '1.0.0',\n source: 'remote' as const,\n manifest: {\n id: plugin.id,\n name: plugin.name,\n version: plugin.version || '1.0.0',\n type: 'ui',\n description: plugin.description || '',\n capabilities: plugin.capabilities || {},\n channels: plugin.channels || ['default'],\n render: (plugin.render && typeof plugin.render === 'object') ? plugin.render : { mode: 'react-native' as const, component: plugin.name || plugin.id },\n entry_point: plugin.entry_point || plugin.name,\n sandbox: plugin.sandbox !== false,\n requires_auth: plugin.requires_auth !== false,\n } as PluginManifest,\n }));\n\n setPlugins(uiPlugins);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[useRemotePlugins] Failed to fetch plugins:', message);\n setError(message);\n setPlugins([]); // Clear plugins on error\n } finally {\n setLoading(false);\n setRefreshing(false);\n }\n }, [solutionId]);\n\n const refresh = useCallback(async () => {\n setRefreshing(true);\n await fetchRemotePlugins();\n }, [fetchRemotePlugins]);\n\n useEffect(() => {\n setLoading(true);\n fetchRemotePlugins();\n }, [fetchRemotePlugins]);\n\n return {\n plugins,\n loading,\n error,\n refreshing,\n refresh,\n };\n}\n"],"mappings":"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","names":["prevTenant","useEffect","useEffect","useEffect","useState"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureDeviceToolsLoaded,
|
|
3
3
|
resetDeviceToolLoader
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-56Z2X5ZB.mjs";
|
|
5
5
|
export {
|
|
6
6
|
ensureDeviceToolsLoaded,
|
|
7
7
|
resetDeviceToolLoader
|
|
8
8
|
};
|
|
9
|
-
//# sourceMappingURL=loader-
|
|
9
|
+
//# sourceMappingURL=loader-3PQLHEEW.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai-mobile/plugin-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "ADAS plugin runtime for React Native hosts — plugin SDK, bundle pipeline with integrity verification, host bridge, surfaces. No Expo dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "tsup"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ateam-ai-mobile/core": "0.1.
|
|
12
|
+
"@ateam-ai-mobile/core": "0.1.16",
|
|
13
13
|
"zustand": "^5.0.0"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
package/src/api/plugins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SurfacePlacement } from '../registry/types';
|
|
1
2
|
import { apiFetch } from '@ateam-ai-mobile/core';
|
|
2
3
|
import { getCoreApiUrl } from '@ateam-ai-mobile/core';
|
|
3
4
|
import { pluginBundleUrl } from '../bundles/bundleUrl';
|
|
@@ -9,6 +10,9 @@ import { pluginBundleUrl } from '../bundles/bundleUrl';
|
|
|
9
10
|
export interface PluginSurfaceMeta {
|
|
10
11
|
type: 'drawer' | 'card' | 'fullscreen' | 'header' | 'ambient' | 'nudge';
|
|
11
12
|
visibility?: 'always' | 'user' | 'engine';
|
|
13
|
+
/** Where the surface sits in the host shell. (Was only in the app's copy of
|
|
14
|
+
* this type, ateam-mobile 674db281.) */
|
|
15
|
+
placement?: SurfacePlacement;
|
|
12
16
|
trigger?: { from?: string; gesture?: string };
|
|
13
17
|
icon?: string;
|
|
14
18
|
title?: string;
|
package/src/host.ts
CHANGED
|
@@ -216,6 +216,28 @@ export const nativePluginRegistry = {
|
|
|
216
216
|
},
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Does this plugin render NATIVELY (the SDK's <Plugin>) rather than in a WebView?
|
|
221
|
+
* The ONE answer for every place a host mounts a plugin: a host-registered
|
|
222
|
+
* component (by component name or plugin id), a bundle URL, or a declared RN
|
|
223
|
+
* component in 'adaptive' / 'react-native' mode. (A declared component in
|
|
224
|
+
* 'iframe' mode is NOT treated as native here — unchanged from the app's
|
|
225
|
+
* version; usePlugin's own bundle derivation is a separate question.)
|
|
226
|
+
* (Moved from ateam-mobile src/plugins/registry.ts, 52f8722a.)
|
|
227
|
+
*/
|
|
228
|
+
export function rendersNative(pluginId: string, manifest: { render?: any } | null | undefined): boolean {
|
|
229
|
+
const render = manifest?.render;
|
|
230
|
+
const rnComponent = render?.reactNative?.component || render?.component || '';
|
|
231
|
+
const hasBundleUrl = !!(render?.bundleUrl || render?.reactNative?.bundleUrl);
|
|
232
|
+
const isNativeMode = render?.mode === 'adaptive' || render?.mode === 'react-native';
|
|
233
|
+
return (
|
|
234
|
+
nativePluginRegistry.has(rnComponent) ||
|
|
235
|
+
nativePluginRegistry.has(pluginId) ||
|
|
236
|
+
hasBundleUrl ||
|
|
237
|
+
(!!rnComponent && isNativeMode)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
// ── Extra bundle modules ───────────────────────────────────────────────────
|
|
220
242
|
// Remote bundles are built with externals resolved by the require shim in
|
|
221
243
|
// bundleManager. react / react-native / @adas/plugin-sdk are always provided;
|