@nihinmedia/medgen-react-native 1.0.9

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/LICENSE ADDED
@@ -0,0 +1,62 @@
1
+ MedGen Japan React Native SDK — Partner License Agreement
2
+
3
+ Copyright (c) 2026 Nihin Media K. K., Ltd. All rights reserved.
4
+
5
+ This software ("SDK") is licensed, not sold, to organisations ("Partner") that
6
+ hold a current written integration agreement with Nihin Media K. K., Ltd.
7
+ ("MedGen JAPAN"). If you do not hold such an agreement, you have no licence to this
8
+ SDK and must not use, copy or distribute it.
9
+
10
+ 1. GRANT
11
+ Subject to the integration agreement and to these terms, MedGen JAPAN grants
12
+ Partner a non-exclusive, non-transferable, revocable licence to install and
13
+ use the SDK solely to embed the MedGen JAPAN clinical assistant inside Partner's
14
+ own applications.
15
+
16
+ 2. RESTRICTIONS
17
+ Partner must not:
18
+ (a) sublicense, resell, or redistribute the SDK on its own, whether or not
19
+ for a fee, except as embedded within Partner's own application;
20
+ (b) reverse engineer, decompile or disassemble the SDK, except to the extent
21
+ that restriction is prohibited by applicable law;
22
+ (c) remove or obscure MedGen JAPAN attribution, except where the
23
+ `hide_attribution` capability has been granted in writing;
24
+ (d) use the SDK to build a competing clinical question-answering service; or
25
+ (e) use the SDK other than through its documented public interface.
26
+
27
+ 3. CREDENTIALS AND KEYS
28
+ Partner is responsible for the security of its signing keys and must keep
29
+ the assertion signing key on Partner-controlled servers. Signing keys must
30
+ never be embedded in an application binary. Partner must notify MedGen JAPAN
31
+ without undue delay on becoming aware of any compromise.
32
+
33
+ 4. DATA PROTECTION
34
+ The SDK transmits clinical queries and identity assertions to MedGen JAPAN.
35
+ Partner must ensure it has the lawful basis and any consents required to do
36
+ so, and must comply with the Act on the Protection of Personal Information
37
+ (APPI) and any other applicable law. Handling of personal data between the
38
+ parties is governed by the integration agreement.
39
+
40
+ 5. NO WARRANTY
41
+ THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. THE SDK IS NOT A
44
+ MEDICAL DEVICE AND MUST NOT BE RELIED ON AS THE SOLE BASIS FOR ANY CLINICAL
45
+ DECISION.
46
+
47
+ 6. LIMITATION OF LIABILITY
48
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, MEDGEN JAPAN SHALL NOT BE LIABLE FOR ANY
49
+ INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES, OR FOR LOST PROFITS
50
+ OR DATA, ARISING OUT OF OR RELATING TO THE SDK. Nothing in this licence
51
+ excludes liability that cannot lawfully be excluded.
52
+
53
+ 7. TERMINATION
54
+ This licence terminates automatically on termination of the integration
55
+ agreement, or on Partner's breach of these terms. On termination Partner
56
+ must cease distributing applications containing the SDK in new releases.
57
+
58
+ 8. GOVERNING LAW
59
+ This licence is governed by the laws of Japan, with exclusive jurisdiction
60
+ in the Tokyo District Court.
61
+
62
+ Contact: partners@nihinmedia.jp
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @nihinmedia/medgen-react-native
2
+
3
+ Embed MedGen's clinical assistant inside your React Native app. Your users
4
+ reach MedGen without creating a separate account: your backend signs a short
5
+ assertion, MedGen exchanges it for its own session.
6
+
7
+ > **New here?** The quick start below gets the surface open, but you also need
8
+ > a server-side endpoint that signs the assertion. Ask your MedGen
9
+ > representative for the full integration guide, which covers that endpoint,
10
+ > every error code and a pre-launch checklist.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @nihinmedia/medgen-react-native react-native-webview react-native-keychain
16
+ cd ios && pod install
17
+ ```
18
+
19
+ The package is public on npm, but **use requires a current integration
20
+ agreement with Nihin Media**. You also need partner
21
+ credentials (a `partnerId` and a key pair) before the SDK will do anything:
22
+ without them every exchange fails with `PARTNER_NOT_ENABLED`.
23
+
24
+ ## Quick start
25
+
26
+ ```tsx
27
+ import { MedGen, MedGenHost, MedGenEnvironment } from '@nihinmedia/medgen-react-native';
28
+
29
+ MedGen.initialize({
30
+ partnerId: 'your-partner-id',
31
+ environment: MedGenEnvironment.Production,
32
+ // Must call YOUR server. Never sign in the app.
33
+ assertionProvider: async () => {
34
+ const res = await fetch('https://your-api.example.com/medgen/assertion', {
35
+ headers: { Authorization: `Bearer ${yourSessionToken}` },
36
+ });
37
+ const { assertion } = await res.json();
38
+ return assertion;
39
+ },
40
+ });
41
+
42
+ // Mount once, near the root.
43
+ export default function App() {
44
+ return (
45
+ <>
46
+ <YourApp />
47
+ <MedGenHost />
48
+ </>
49
+ );
50
+ }
51
+
52
+ // Open the surface.
53
+ await MedGen.present();
54
+ ```
55
+
56
+ ## The one rule that matters
57
+
58
+ **Never ship the private key in the app.** A key inside an app binary can be
59
+ extracted and must be assumed public. `assertionProvider` exists so signing
60
+ stays on your server, where the key is.
61
+
62
+ Assertions are **single-use**. Return a fresh one every call — a cached
63
+ assertion fails with `ASSERTION_REPLAYED`.
64
+
65
+ ## Sign out
66
+
67
+ ```tsx
68
+ await MedGen.signOut(); // call this from YOUR logout
69
+ ```
70
+
71
+ Skipping it leaves a valid MedGen session on the device. On a shared clinical
72
+ device the next user reaches the previous doctor's account and history.
73
+
74
+ ## Capabilities
75
+
76
+ MedGen grants capabilities per partner. Read them rather than assuming:
77
+
78
+ ```tsx
79
+ MedGen.initialize({
80
+ /* ... */
81
+ onEvent: (e) => {
82
+ if (e.name === 'exchange.succeeded') analytics.track('medgen_opened');
83
+ },
84
+ });
85
+ ```
86
+
87
+ The surface hides controls it has no native support for, so a feature you have
88
+ not wired up does not appear as a dead button.
89
+
90
+ ## Native features
91
+
92
+ Optional. Provide only what you support:
93
+
94
+ ```tsx
95
+ MedGen.initialize({
96
+ /* ... */
97
+ nativeHandlers: {
98
+ onShare: async (text) => { await Share.share({ message: text }); return true; },
99
+ onCopy: async ({ text, html }) => { Clipboard.setString(text); return true; },
100
+ },
101
+ });
102
+ ```
103
+
104
+ Passing `html` to the system pasteboard is what lets a doctor paste an answer
105
+ into clinical notes with headings and citations intact.
106
+
107
+ ## Attestation
108
+
109
+ Required only if MedGen has enabled it for you:
110
+
111
+ ```tsx
112
+ attestationProvider: async (challenge) => {
113
+ // iOS: DCAppAttestService attestation object, base64
114
+ // Android: Play Integrity token
115
+ return await YourAttestationModule.getToken(challenge);
116
+ },
117
+ ```
118
+
119
+ `challenge` is the assertion JWT itself, which binds the attestation to this
120
+ exchange — a captured token cannot be replayed against a different one. Pass it
121
+ to the platform API **unchanged**; the server hashes it itself, so hashing or
122
+ re-encoding it here breaks verification.
123
+
124
+ ## Errors
125
+
126
+ Every failure is a `MedGenError` with a `code` and a `retryable` flag:
127
+
128
+ ```tsx
129
+ try {
130
+ await MedGen.present();
131
+ } catch (e) {
132
+ if (e.code === 'EMAIL_COLLISION_PENDING') { /* not an error — see below */ }
133
+ else if (e.retryable) showRetry();
134
+ else showSupport(e.supportReference);
135
+ }
136
+ ```
137
+
138
+ **`emailCollisionPending` is a success, not a failure.** It means the asserted
139
+ email already belongs to a MedGen account you did not create. The session is
140
+ valid and the doctor can use MedGen immediately; MedGen offers account linking
141
+ from inside the surface. Do not block on it.
142
+
143
+ ## Requirements
144
+
145
+ | | |
146
+ |---|---|
147
+ | React Native | ≥ 0.71 |
148
+ | iOS | 13.4+ |
149
+ | Android | API 24+ |
150
+
151
+ ## Support
152
+
153
+ Contact your MedGen representative with the `supportReference` from any error,
154
+ or email **contact@nihinmedia.jp**.
155
+
156
+ Security reports go to the same address. Please report privately rather than in
157
+ any public forum.
158
+
159
+ ## Licence
160
+
161
+ Proprietary — Nihin Media Co., Ltd. Use requires a current integration
162
+ agreement. The full terms are in the `LICENSE` file inside this package.
package/SECURITY.md ADDED
@@ -0,0 +1,101 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Email **contact@nihinmedia.jp** with `[MedGen RN SDK]` in the subject.
6
+ Please report privately by email rather than in any public forum.
7
+
8
+ Include where you can: affected SDK version, platform (iOS/Android), a
9
+ description of the issue, and the steps or proof-of-concept needed to
10
+ reproduce it.
11
+
12
+ | | |
13
+ |---|---|
14
+ | Acknowledgement | within 2 business days |
15
+ | Initial assessment | within 5 business days |
16
+ | Fix target — critical / high | 7 / 30 days |
17
+
18
+ We will keep you updated through triage and tell you when a fix ships. We are
19
+ happy to credit you in the release notes unless you would rather stay
20
+ anonymous. Please give us a reasonable window to release a fix before any
21
+ public disclosure.
22
+
23
+ ## Supported versions
24
+
25
+ | Version | Supported |
26
+ |---|---|
27
+ | 1.x | ✅ |
28
+ | < 1.0 | ❌ |
29
+
30
+ ## Security model — what the SDK guarantees, and what you must do
31
+
32
+ The SDK is one half of the security boundary. Several properties depend on the
33
+ partner's own integration, and no SDK change can enforce them for you.
34
+
35
+ ### The SDK's side
36
+
37
+ - **Assertion signing never happens in the app.** The SDK has no signing key
38
+ and cannot mint an assertion; it can only call your `assertionProvider`.
39
+ - **Credentials are stored in the platform secure store** (Keychain /
40
+ Android Keystore, via `react-native-keychain`), with
41
+ `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` so a session cannot travel to another
42
+ device in a cloud backup.
43
+ - **Session tokens never appear in a URL.** Only the single-use, ~30-second
44
+ handoff code is put in the embed URL, because URLs leak into logs, history
45
+ and `Referer` headers.
46
+ - **The WebView is locked to MedGen origins.** External links — citations,
47
+ references — are handed to the system browser rather than rendered inside
48
+ the authenticated surface.
49
+ - **The native bridge is an allowlist.** Only the documented methods are
50
+ dispatched; anything else is ignored, so a compromised page cannot reach
51
+ arbitrary native functionality. Capability-gated methods additionally check
52
+ the grants returned by the server.
53
+ - **Local file access is disabled** in the WebView, along with mixed content
54
+ and automatic window opening.
55
+
56
+ ### Your side
57
+
58
+ - **Keep the signing key on your server.** A private key inside an app binary
59
+ can be extracted and must be assumed public. This is the single most
60
+ important rule in the integration.
61
+ - **Authenticate the user before minting an assertion.** Your assertion
62
+ endpoint asserts an identity to MedGen; if it can be called without a valid
63
+ session of your own, anyone can impersonate any of your users.
64
+ - **Return a fresh assertion every call.** They are single-use and live 120
65
+ seconds.
66
+ - **Call `MedGen.signOut()` from your own logout.** Skipping it leaves a valid
67
+ MedGen session on the device — a real exposure on shared clinical devices,
68
+ where the next user would reach the previous doctor's account and history.
69
+ - **Do not point `apiBaseUrl` / `embedBaseUrl` at a non-MedGen host** in a
70
+ production build. They exist for MedGen-supplied test environments. They
71
+ also relax nothing on their own — but a host you do not control receives the
72
+ assertion.
73
+ - **Keep the SDK up to date**, and treat `react-native-webview` updates as
74
+ security updates.
75
+
76
+ ## Requirements on the MedGen embed host
77
+
78
+ These are MedGen's own obligations, recorded here because the SDK's guarantees
79
+ depend on them:
80
+
81
+ - **Emit lowercase host URLs.** `react-native-webview` matches its origin
82
+ whitelist case-sensitively while normalising elsewhere, so a mixed-case
83
+ redirect can cause the library to open the authenticated surface in the
84
+ system browser before the SDK's handler runs.
85
+ - **Do not embed cross-origin iframes in the surface.** The bridge nonce is
86
+ injected main-frame-only and a subframe cannot reach it, but a hostile frame
87
+ remains an unnecessary foothold.
88
+ - **Treat the handoff code as single-use with a short TTL.**
89
+
90
+ ## Audit
91
+
92
+ The SDK was independently audited before its first partner release. All
93
+ findings were remediated prior to publication. Partners under an integration
94
+ agreement can request the full report from their MedGen representative.
95
+
96
+ ## Handling of clinical data
97
+
98
+ Queries typed into the surface are transmitted to MedGen for answering. The
99
+ SDK itself persists no clinical content on the device; the chat history lives
100
+ in the MedGen session. Your obligations for personal data are set out in your
101
+ integration agreement and in APPI.
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+
7
+ exports.originOf = exports.isSameOrigin = exports.decideNavigation = exports.MedGenWebView = void 0;
8
+
9
+ var _react = _interopRequireWildcard(require("react"));
10
+
11
+ var _reactNative = require("react-native");
12
+
13
+ var _reactNativeWebview = require("react-native-webview");
14
+
15
+ var _bridge = require("./bridge");
16
+
17
+ var _client = require("./client");
18
+
19
+ function _interopRequireWildcard(e, t) {
20
+ if ("function" == typeof WeakMap) var r = new WeakMap, n = new WeakMap;
21
+ return (_interopRequireWildcard = function(e, t) {
22
+ if (!t && e && e.__esModule) return e;
23
+ var o, i, f = {
24
+ __proto__: null,
25
+ default: e
26
+ };
27
+ if (null === e || "object" != typeof e && "function" != typeof e) return f;
28
+ if (o = t ? n : r) {
29
+ if (o.has(e)) return o.get(e);
30
+ o.set(e, f);
31
+ }
32
+ for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]);
33
+ return f;
34
+ })(e, t);
35
+ }
36
+
37
+ const WebView = _reactNativeWebview.WebView;
38
+
39
+ const originOf = u => {
40
+ const m = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]+/i.exec(u);
41
+ return m ? m[0].toLowerCase() : "";
42
+ };
43
+
44
+ exports.originOf = originOf;
45
+
46
+ const decideNavigation = (url, base, isTopFrame) => {
47
+ if (isSameOrigin(url, base)) return "load";
48
+ if (isTopFrame === false) return "block";
49
+ if (!/^https?:\/\//i.test(url)) return "block";
50
+ return "external";
51
+ };
52
+
53
+ exports.decideNavigation = decideNavigation;
54
+
55
+ const openExternally = async url => {
56
+ try {
57
+ if (!await _reactNative.Linking.canOpenURL(url)) {
58
+ (0, _client.emit)("surface.externalLinkFailed", {
59
+ url: url,
60
+ reason: "unsupported"
61
+ });
62
+ return false;
63
+ }
64
+ await _reactNative.Linking.openURL(url);
65
+ (0, _client.emit)("surface.externalLinkOpened", {
66
+ url: url
67
+ });
68
+ return true;
69
+ } catch (error) {
70
+ (0, _client.emit)("surface.externalLinkFailed", {
71
+ url: url,
72
+ reason: error instanceof Error ? error.message : "unknown"
73
+ });
74
+ return false;
75
+ }
76
+ };
77
+
78
+ const makeNonce = () => `${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
79
+
80
+ const isSameOrigin = (target, base) => {
81
+ if (target === "about:blank") return true;
82
+ const b = originOf(base);
83
+ return b !== "" && originOf(target) === b;
84
+ };
85
+
86
+ exports.isSameOrigin = isSameOrigin;
87
+
88
+ const MedGenWebView = ({url: url, handlers: handlers, capabilities: capabilities, onClose: onClose}) => {
89
+ const ref = (0, _react.useRef)(null);
90
+ const nonce = (0, _react.useMemo)(() => makeNonce(), [ url ]);
91
+ const isMedGenOrigin = (0, _react.useCallback)(target => isSameOrigin(target, (0,
92
+ _client.embedBase)()), []);
93
+ const onShouldStartLoadWithRequest = (0, _react.useCallback)(req => {
94
+ const decision = decideNavigation(req.url, (0, _client.embedBase)(), req.isTopFrame);
95
+ if (decision === "load") return true;
96
+ if (decision === "external") void openExternally(req.url);
97
+ return false;
98
+ }, [ isMedGenOrigin ]);
99
+ const onMessage = (0, _react.useCallback)(event => {
100
+ if (!isMedGenOrigin(event.nativeEvent.url)) return;
101
+ void (0, _bridge.handleBridgeMessage)(event.nativeEvent.data, replyJson => {
102
+ const safe = JSON.stringify(replyJson).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
103
+ ref.current?.injectJavaScript(`window.__medgen && window.__medgen.receive(${safe}); true;`);
104
+ }, {
105
+ handlers: handlers,
106
+ capabilities: capabilities,
107
+ onClose: onClose,
108
+ nonce: nonce
109
+ });
110
+ }, [ isMedGenOrigin, handlers, capabilities, onClose, nonce ]);
111
+ return _react.default.createElement(WebView, {
112
+ ref: ref,
113
+ source: {
114
+ uri: url
115
+ },
116
+ style: styles.web,
117
+ injectedJavaScriptBeforeContentLoaded: `window.__medgenNonce=${JSON.stringify(nonce)};true;`,
118
+ injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true,
119
+ originWhitelist: [ originOf((0, _client.embedBase)()) ],
120
+ onShouldStartLoadWithRequest: onShouldStartLoadWithRequest,
121
+ onMessage: onMessage,
122
+ allowFileAccess: false,
123
+ allowUniversalAccessFromFileURLs: false,
124
+ mixedContentMode: "never",
125
+ javaScriptCanOpenWindowsAutomatically: false,
126
+ setSupportMultipleWindows: false,
127
+ onOpenWindow: event => {
128
+ const url = event?.nativeEvent?.targetUrl;
129
+ if (!url) return;
130
+ const decision = decideNavigation(url, (0, _client.embedBase)(), true);
131
+ if (decision === "load") {
132
+ ref.current?.injectJavaScript(`window.location.assign(${JSON.stringify(url)});true;`);
133
+ } else if (decision === "external") {
134
+ void openExternally(url);
135
+ }
136
+ },
137
+ onError: event => {
138
+ (0, _client.emit)("surface.loadFailed", {
139
+ description: event?.nativeEvent?.description,
140
+ code: event?.nativeEvent?.code
141
+ });
142
+ },
143
+ onHttpError: event => {
144
+ (0, _client.emit)("surface.loadFailed", {
145
+ statusCode: event?.nativeEvent?.statusCode
146
+ });
147
+ },
148
+ sharedCookiesEnabled: true,
149
+ thirdPartyCookiesEnabled: false,
150
+ startInLoadingState: true,
151
+ renderLoading: () => _react.default.createElement(_reactNative.View, {
152
+ style: styles.loading
153
+ }, _react.default.createElement(_reactNative.ActivityIndicator, {
154
+ size: "large"
155
+ })),
156
+ bounces: false,
157
+ overScrollMode: "never"
158
+ });
159
+ };
160
+
161
+ exports.MedGenWebView = MedGenWebView;
162
+
163
+ const styles = _reactNative.StyleSheet.create({
164
+ web: {
165
+ flex: 1
166
+ },
167
+ loading: {
168
+ ..._reactNative.StyleSheet.absoluteFillObject,
169
+ alignItems: "center",
170
+ justifyContent: "center"
171
+ }
172
+ });
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+
7
+ exports.handleBridgeMessage = handleBridgeMessage;
8
+
9
+ var _client = require("./client");
10
+
11
+ const V = 1;
12
+
13
+ const ok = (id, payload) => JSON.stringify({
14
+ v: V,
15
+ id: id,
16
+ ok: true,
17
+ payload: payload
18
+ });
19
+
20
+ const fail = (id, code, message) => JSON.stringify({
21
+ v: V,
22
+ id: id,
23
+ ok: false,
24
+ error: {
25
+ code: code,
26
+ message: message
27
+ }
28
+ });
29
+
30
+ async function handleBridgeMessage(raw, reply, ctx) {
31
+ let msg;
32
+ try {
33
+ msg = JSON.parse(raw);
34
+ } catch {
35
+ return;
36
+ }
37
+ if (msg.v !== V || !msg.id || !msg.method) return;
38
+ const {handlers: handlers, capabilities: capabilities, onClose: onClose, nonce: nonce} = ctx;
39
+ if (typeof msg.nonce !== "string" || msg.nonce.length !== nonce.length || msg.nonce !== nonce) {
40
+ return;
41
+ }
42
+ const {id: id, method: method} = msg;
43
+ const payload = msg.payload ?? {};
44
+ try {
45
+ switch (method) {
46
+ case "system.ready":
47
+ reply(ok(id, {
48
+ platform: "react-native",
49
+ version: _client.SDK_VERSION,
50
+ features: {
51
+ share: Boolean(handlers.onShare) && capabilities.share,
52
+ clipboard: Boolean(handlers.onCopy) && capabilities.clipboard,
53
+ speech: Boolean(handlers.onSpeechStart) && capabilities.dictation,
54
+ fileUpload: Boolean(handlers.onFilePick) && capabilities.fileUpload
55
+ }
56
+ }));
57
+ return;
58
+
59
+ case "system.dismiss":
60
+ onClose();
61
+ reply(ok(id));
62
+ return;
63
+
64
+ case "share.content":
65
+ {
66
+ if (!capabilities.share) return reply(fail(id, "FORBIDDEN", "share not granted"));
67
+ if (!handlers.onShare) return reply(fail(id, "UNAVAILABLE", "No share handler"));
68
+ const completed = await handlers.onShare(String(payload.text ?? ""));
69
+ reply(ok(id, {
70
+ completed: completed
71
+ }));
72
+ return;
73
+ }
74
+
75
+ case "clipboard.write":
76
+ {
77
+ if (!capabilities.clipboard) return reply(fail(id, "FORBIDDEN", "clipboard not granted"));
78
+ if (!handlers.onCopy) return reply(fail(id, "UNAVAILABLE", "No clipboard handler"));
79
+ const html = capabilities.richClipboard ? payload.html : undefined;
80
+ const written = await handlers.onCopy({
81
+ text: String(payload.text ?? ""),
82
+ html: html
83
+ });
84
+ reply(ok(id, {
85
+ written: written
86
+ }));
87
+ return;
88
+ }
89
+
90
+ case "speech.start":
91
+ {
92
+ if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
93
+ if (!handlers.onSpeechStart) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
94
+ await handlers.onSpeechStart();
95
+ reply(ok(id, {
96
+ listening: true
97
+ }));
98
+ return;
99
+ }
100
+
101
+ case "speech.stop":
102
+ {
103
+ if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
104
+ if (!handlers.onSpeechStop) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
105
+ const transcript = await handlers.onSpeechStop();
106
+ reply(ok(id, {
107
+ transcript: transcript
108
+ }));
109
+ return;
110
+ }
111
+
112
+ case "file.pick":
113
+ case "file.capture":
114
+ {
115
+ if (!capabilities.fileUpload) return reply(fail(id, "FORBIDDEN", "file upload not granted"));
116
+ if (!handlers.onFilePick) return reply(ok(id, {
117
+ files: []
118
+ }));
119
+ const files = await handlers.onFilePick();
120
+ reply(ok(id, {
121
+ files: files
122
+ }));
123
+ return;
124
+ }
125
+
126
+ default:
127
+ return;
128
+ }
129
+ } catch (e) {
130
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
131
+ console.warn("[MedGen] native handler failed:", e);
132
+ }
133
+ reply(fail(id, "HANDLER_ERROR", "Handler failed"));
134
+ }
135
+ }