@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 +62 -0
- package/README.md +162 -0
- package/SECURITY.md +101 -0
- package/lib/commonjs/MedGenWebView.js +172 -0
- package/lib/commonjs/bridge.js +135 -0
- package/lib/commonjs/client.js +350 -0
- package/lib/commonjs/index.js +199 -0
- package/lib/commonjs/types.js +27 -0
- package/lib/module/MedGenWebView.js +137 -0
- package/lib/module/bridge.js +127 -0
- package/lib/module/client.js +298 -0
- package/lib/module/index.js +147 -0
- package/lib/module/types.js +17 -0
- package/lib/typescript/MedGenWebView.d.ts +18 -0
- package/lib/typescript/bridge.d.ts +12 -0
- package/lib/typescript/client.d.ts +28 -0
- package/lib/typescript/index.d.ts +21 -0
- package/lib/typescript/types.d.ts +108 -0
- package/package.json +97 -0
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
|
+
}
|