@nihinmedia/medgen-react-native 1.0.12
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 +221 -0
- package/SECURITY.md +101 -0
- package/lib/commonjs/MedGenWebView.js +201 -0
- package/lib/commonjs/bridge.js +208 -0
- package/lib/commonjs/client.js +445 -0
- package/lib/commonjs/index.js +204 -0
- package/lib/commonjs/native.js +201 -0
- package/lib/commonjs/pdfLinks.js +57 -0
- package/lib/commonjs/types.js +27 -0
- package/lib/module/MedGenWebView.js +166 -0
- package/lib/module/bridge.js +182 -0
- package/lib/module/client.js +391 -0
- package/lib/module/index.js +152 -0
- package/lib/module/native.js +171 -0
- package/lib/module/pdfLinks.js +45 -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 +30 -0
- package/lib/typescript/index.d.ts +23 -0
- package/lib/typescript/native.d.ts +38 -0
- package/lib/typescript/pdfLinks.d.ts +14 -0
- package/lib/typescript/types.d.ts +182 -0
- package/package.json +116 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { SDK_VERSION } from "./client";
|
|
2
|
+
|
|
3
|
+
import * as native from "./native";
|
|
4
|
+
|
|
5
|
+
import { sanitizeLinks } from "./pdfLinks";
|
|
6
|
+
|
|
7
|
+
const V = 1;
|
|
8
|
+
|
|
9
|
+
const ok = (id, payload) => JSON.stringify({
|
|
10
|
+
v: V,
|
|
11
|
+
id: id,
|
|
12
|
+
ok: true,
|
|
13
|
+
payload: payload
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const fail = (id, code, message) => JSON.stringify({
|
|
17
|
+
v: V,
|
|
18
|
+
id: id,
|
|
19
|
+
ok: false,
|
|
20
|
+
error: {
|
|
21
|
+
code: code,
|
|
22
|
+
message: message
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export async function handleBridgeMessage(raw, reply, ctx) {
|
|
27
|
+
const trace = (why, detail) => {
|
|
28
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
29
|
+
console.warn(`[MedGen] bridge dropped a frame: ${why}`, detail ?? "");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
let msg;
|
|
33
|
+
try {
|
|
34
|
+
msg = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (msg.v !== V) {
|
|
39
|
+
trace(`protocol version ${String(msg.v)} (this SDK speaks v${V})`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!msg.id || !msg.method) {
|
|
43
|
+
trace("missing id or method", msg);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const {handlers: handlers, capabilities: capabilities, onClose: onClose, nonce: nonce} = ctx;
|
|
47
|
+
if (typeof msg.nonce !== "string" || msg.nonce.length !== nonce.length || msg.nonce !== nonce) {
|
|
48
|
+
trace(typeof msg.nonce !== "string" ? `no nonce on "${String(msg.method)}" — the surface is not sending ` + "window.__medgenNonce. Every native call will be dropped." : `nonce mismatch on "${String(msg.method)}" — a subframe, or a stale page`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const {id: id, method: method} = msg;
|
|
52
|
+
const payload = msg.payload ?? {};
|
|
53
|
+
try {
|
|
54
|
+
switch (method) {
|
|
55
|
+
case "system.ready":
|
|
56
|
+
reply(ok(id, {
|
|
57
|
+
platform: "react-native",
|
|
58
|
+
version: SDK_VERSION,
|
|
59
|
+
features: {
|
|
60
|
+
share: capabilities.share && (Boolean(handlers.onShare) || native.canShareText()),
|
|
61
|
+
clipboard: capabilities.clipboard && (Boolean(handlers.onCopy) || native.canCopy()),
|
|
62
|
+
speech: Boolean(handlers.onSpeechStart) && capabilities.dictation,
|
|
63
|
+
fileUpload: Boolean(handlers.onFilePick) && capabilities.fileUpload,
|
|
64
|
+
downloadPdf: capabilities.downloadPdf && (Boolean(handlers.onSavePdf) || native.canRenderPdf())
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
return;
|
|
68
|
+
|
|
69
|
+
case "system.dismiss":
|
|
70
|
+
onClose();
|
|
71
|
+
reply(ok(id));
|
|
72
|
+
return;
|
|
73
|
+
|
|
74
|
+
case "share.content":
|
|
75
|
+
{
|
|
76
|
+
if (!capabilities.share) return reply(fail(id, "FORBIDDEN", "share not granted"));
|
|
77
|
+
const text = String(payload.text ?? "");
|
|
78
|
+
const completed = handlers.onShare ? await handlers.onShare(text) : await native.shareText(text);
|
|
79
|
+
reply(ok(id, {
|
|
80
|
+
completed: completed
|
|
81
|
+
}));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "clipboard.write":
|
|
86
|
+
{
|
|
87
|
+
if (!capabilities.clipboard) return reply(fail(id, "FORBIDDEN", "clipboard not granted"));
|
|
88
|
+
const text = String(payload.text ?? "");
|
|
89
|
+
const html = capabilities.richClipboard ? payload.html : undefined;
|
|
90
|
+
const written = handlers.onCopy ? await handlers.onCopy({
|
|
91
|
+
text: text,
|
|
92
|
+
html: html
|
|
93
|
+
}) : await native.writeClipboard(text, html);
|
|
94
|
+
reply(ok(id, {
|
|
95
|
+
written: written
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "speech.start":
|
|
101
|
+
{
|
|
102
|
+
if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
|
|
103
|
+
if (!handlers.onSpeechStart) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
|
|
104
|
+
await handlers.onSpeechStart();
|
|
105
|
+
reply(ok(id, {
|
|
106
|
+
listening: true
|
|
107
|
+
}));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case "speech.stop":
|
|
112
|
+
{
|
|
113
|
+
if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
|
|
114
|
+
if (!handlers.onSpeechStop) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
|
|
115
|
+
const transcript = await handlers.onSpeechStop();
|
|
116
|
+
reply(ok(id, {
|
|
117
|
+
transcript: transcript
|
|
118
|
+
}));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "pdf.save":
|
|
123
|
+
{
|
|
124
|
+
const html = String(payload.html ?? "");
|
|
125
|
+
const fileName = String(payload.fileName ?? "MedGen.pdf");
|
|
126
|
+
const links = sanitizeLinks(payload.links);
|
|
127
|
+
if (handlers.onSavePdf) {
|
|
128
|
+
const saved = await handlers.onSavePdf({
|
|
129
|
+
html: html,
|
|
130
|
+
fileName: fileName,
|
|
131
|
+
links: links
|
|
132
|
+
});
|
|
133
|
+
return reply(ok(id, {
|
|
134
|
+
saved: saved
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
const path = await native.renderPdf(html, fileName, links);
|
|
138
|
+
if (!path) {
|
|
139
|
+
return reply(fail(id, "UNAVAILABLE", "PDF rendering needs react-native-html-to-pdf, or an onSavePdf handler"));
|
|
140
|
+
}
|
|
141
|
+
if (capabilities.downloadToDevice) {
|
|
142
|
+
const result = await native.saveToDownloads(path, fileName);
|
|
143
|
+
if (result.saved) return reply(ok(id, {
|
|
144
|
+
saved: true,
|
|
145
|
+
uri: result.uri
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
const shared = await native.shareFile(path, "application/pdf", fileName);
|
|
149
|
+
reply(ok(id, {
|
|
150
|
+
saved: shared.saved,
|
|
151
|
+
uri: shared.uri,
|
|
152
|
+
reason: shared.reason
|
|
153
|
+
}));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "file.pick":
|
|
158
|
+
case "file.capture":
|
|
159
|
+
{
|
|
160
|
+
if (!capabilities.fileUpload) return reply(fail(id, "FORBIDDEN", "file upload not granted"));
|
|
161
|
+
if (!handlers.onFilePick) return reply(ok(id, {
|
|
162
|
+
files: []
|
|
163
|
+
}));
|
|
164
|
+
const files = await handlers.onFilePick();
|
|
165
|
+
reply(ok(id, {
|
|
166
|
+
files: files
|
|
167
|
+
}));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
default:
|
|
172
|
+
trace(`unknown method "${String(method)}" — SDK v${SDK_VERSION}`);
|
|
173
|
+
reply(fail(id, "UNSUPPORTED", `Unknown method: ${String(method)}`));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
+
console.warn("[MedGen] native handler failed:", e);
|
|
179
|
+
}
|
|
180
|
+
reply(fail(id, "HANDLER_ERROR", "Handler failed"));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { registerNativeModules } from "./native";
|
|
4
|
+
|
|
5
|
+
import * as Keychain from "react-native-keychain";
|
|
6
|
+
|
|
7
|
+
import { MedGenError } from "./types";
|
|
8
|
+
|
|
9
|
+
const KEYCHAIN_SERVICE = "jp.nihinmedia.medgen";
|
|
10
|
+
|
|
11
|
+
const ASSERTION_TIMEOUT_MS = 1e4;
|
|
12
|
+
|
|
13
|
+
const ATTESTATION_TIMEOUT_MS = 15e3;
|
|
14
|
+
|
|
15
|
+
export const SDK_VERSION = "1.0.12";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API = {
|
|
18
|
+
development: "https://dev.sugoimed.com",
|
|
19
|
+
staging: "https://staging.sugoimed.com",
|
|
20
|
+
production: "https://medgen.nihinmedia.jp"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_EMBED = {
|
|
24
|
+
development: "https://dev.sugoimed.com",
|
|
25
|
+
staging: "https://staging.sugoimed.com",
|
|
26
|
+
production: "https://medgen.nihinmedia.jp"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let config = null;
|
|
30
|
+
|
|
31
|
+
export function configure(cfg) {
|
|
32
|
+
config = cfg;
|
|
33
|
+
if (cfg.nativeModules) registerNativeModules(cfg.nativeModules);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resetConfig() {
|
|
37
|
+
config = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function requireConfig() {
|
|
41
|
+
if (!config) {
|
|
42
|
+
throw new MedGenError("NOT_INITIALIZED", "Call MedGen.initialize() before present()");
|
|
43
|
+
}
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireHttps(url, field) {
|
|
48
|
+
const lower = url.toLowerCase();
|
|
49
|
+
const isLocal = lower.startsWith("http://localhost") || lower.startsWith("http://127.0.0.1");
|
|
50
|
+
if (!lower.startsWith("https://") && !isLocal) {
|
|
51
|
+
throw new MedGenError("NOT_INITIALIZED", `${field} must use https:// — refusing to send an assertion over plaintext`);
|
|
52
|
+
}
|
|
53
|
+
return url.replace(/\/+$/, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const apiBase = () => {
|
|
57
|
+
const cfg = requireConfig();
|
|
58
|
+
const base = cfg.apiBaseUrl ?? DEFAULT_API[cfg.environment] ?? DEFAULT_API.staging;
|
|
59
|
+
return requireHttps(base, "apiBaseUrl");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const embedBase = () => {
|
|
63
|
+
const cfg = requireConfig();
|
|
64
|
+
const base = cfg.embedBaseUrl ?? DEFAULT_EMBED[cfg.environment] ?? DEFAULT_EMBED.staging;
|
|
65
|
+
return requireHttps(base, "embedBaseUrl");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function emit(name, detail) {
|
|
69
|
+
const handler = config?.onEvent;
|
|
70
|
+
if (!handler) return;
|
|
71
|
+
const event = {
|
|
72
|
+
name: name,
|
|
73
|
+
at: Date.now(),
|
|
74
|
+
detail: detail
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
handler(event);
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function storeCredentials(creds) {
|
|
82
|
+
await Keychain.setGenericPassword("medgen", JSON.stringify(creds), {
|
|
83
|
+
service: KEYCHAIN_SERVICE,
|
|
84
|
+
accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function readCredentials() {
|
|
89
|
+
try {
|
|
90
|
+
const res = await Keychain.getGenericPassword({
|
|
91
|
+
service: KEYCHAIN_SERVICE
|
|
92
|
+
});
|
|
93
|
+
if (!res) return null;
|
|
94
|
+
return JSON.parse(res.password);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function clearCredentials() {
|
|
101
|
+
await Keychain.resetGenericPassword({
|
|
102
|
+
service: KEYCHAIN_SERVICE
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function withTimeout(p, ms, code, what) {
|
|
107
|
+
let timer;
|
|
108
|
+
const timeout = new Promise((_, reject) => {
|
|
109
|
+
timer = setTimeout(() => reject(new MedGenError(code, `${what} exceeded ${ms}ms`, true)), ms);
|
|
110
|
+
});
|
|
111
|
+
return Promise.race([ p, timeout ]).finally(() => clearTimeout(timer));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function fetchAssertion() {
|
|
115
|
+
const {assertionProvider: assertionProvider, assertion: assertion} = requireConfig();
|
|
116
|
+
if (!assertionProvider) {
|
|
117
|
+
if (assertion) return assertion;
|
|
118
|
+
throw new MedGenError("NOT_INITIALIZED", "Set assertionProvider (or, to try the SDK once, assertion) in MedGen.initialize()");
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return await withTimeout(assertionProvider(), ASSERTION_TIMEOUT_MS, "ASSERTION_PROVIDER_TIMEOUT", "Assertion provider");
|
|
122
|
+
} catch (e) {
|
|
123
|
+
if (e instanceof MedGenError) throw e;
|
|
124
|
+
throw new MedGenError("ASSERTION_PROVIDER_FAILED", e instanceof Error ? e.message : "Assertion provider failed", true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function fetchAttestation(assertion) {
|
|
129
|
+
const {attestationProvider: attestationProvider} = requireConfig();
|
|
130
|
+
if (!attestationProvider) return "";
|
|
131
|
+
try {
|
|
132
|
+
return await withTimeout(attestationProvider(assertion), ATTESTATION_TIMEOUT_MS, "ATTESTATION_FAILED", "Attestation provider");
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (e instanceof MedGenError) throw e;
|
|
135
|
+
throw new MedGenError("ATTESTATION_FAILED", e instanceof Error ? e.message : "Attestation provider failed", true);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const CAPABILITY_DEFAULTS = {
|
|
140
|
+
theming: false,
|
|
141
|
+
customLogo: false,
|
|
142
|
+
customMonogram: false,
|
|
143
|
+
hideAttribution: false,
|
|
144
|
+
topbar: true,
|
|
145
|
+
newChat: true,
|
|
146
|
+
sidebar: false,
|
|
147
|
+
networkBanner: true,
|
|
148
|
+
notifications: false,
|
|
149
|
+
referral: false,
|
|
150
|
+
announcements: false,
|
|
151
|
+
appDownloadPrompt: false,
|
|
152
|
+
notes: false,
|
|
153
|
+
questionForum: false,
|
|
154
|
+
workshopVideos: false,
|
|
155
|
+
collections: false,
|
|
156
|
+
ads: false,
|
|
157
|
+
navBar: false,
|
|
158
|
+
settings: false,
|
|
159
|
+
settingsLegal: true,
|
|
160
|
+
settingsTheme: false,
|
|
161
|
+
settingsSupport: false,
|
|
162
|
+
settingsSystemInfo: false,
|
|
163
|
+
settingsLogout: false,
|
|
164
|
+
profileAccount: false,
|
|
165
|
+
profileDeleteAccount: false,
|
|
166
|
+
quickAnswer: true,
|
|
167
|
+
detailedAnswer: true,
|
|
168
|
+
japaneseAnswer: true,
|
|
169
|
+
dictation: false,
|
|
170
|
+
fileUpload: false,
|
|
171
|
+
clipboard: false,
|
|
172
|
+
richClipboard: true,
|
|
173
|
+
share: true,
|
|
174
|
+
downloadToDevice: false,
|
|
175
|
+
closeButton: false,
|
|
176
|
+
downloadPdf: false,
|
|
177
|
+
chatHistory: true,
|
|
178
|
+
bookmarks: false,
|
|
179
|
+
citationsPanel: true,
|
|
180
|
+
referencesShowMore: true,
|
|
181
|
+
directLink: true
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export function parseBranding(raw) {
|
|
185
|
+
const src = raw ?? {};
|
|
186
|
+
const out = {};
|
|
187
|
+
const asset = value => {
|
|
188
|
+
if (!value || typeof value !== "object") return undefined;
|
|
189
|
+
const a = value;
|
|
190
|
+
if (typeof a.url !== "string" || !a.url) return undefined;
|
|
191
|
+
return {
|
|
192
|
+
url: a.url,
|
|
193
|
+
alt: typeof a.alt === "string" ? a.alt : undefined,
|
|
194
|
+
width: typeof a.width === "number" ? a.width : undefined,
|
|
195
|
+
height: typeof a.height === "number" ? a.height : undefined
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
const logo = asset(src.logo);
|
|
199
|
+
if (logo) out.logo = logo;
|
|
200
|
+
const monogram = asset(src.monogram);
|
|
201
|
+
if (monogram) out.monogram = monogram;
|
|
202
|
+
const palette = value => {
|
|
203
|
+
if (!value || typeof value !== "object") return undefined;
|
|
204
|
+
const entries = Object.entries(value).filter(([, v]) => typeof v === "string");
|
|
205
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
206
|
+
};
|
|
207
|
+
const colors = palette(src.colors);
|
|
208
|
+
if (colors) out.colors = colors;
|
|
209
|
+
const colorsDark = palette(src.colorsDark);
|
|
210
|
+
if (colorsDark) out.colorsDark = colorsDark;
|
|
211
|
+
if (src.attribution && typeof src.attribution === "object") {
|
|
212
|
+
out.attribution = {
|
|
213
|
+
hidden: Boolean(src.attribution.hidden)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function parseCapabilities(raw) {
|
|
220
|
+
const src = raw ?? {};
|
|
221
|
+
const pick = (key, fallback) => typeof src[key] === "boolean" ? src[key] : fallback;
|
|
222
|
+
return {
|
|
223
|
+
theming: pick("theming", CAPABILITY_DEFAULTS.theming),
|
|
224
|
+
customLogo: pick("custom_logo", CAPABILITY_DEFAULTS.customLogo),
|
|
225
|
+
customMonogram: pick("custom_monogram", CAPABILITY_DEFAULTS.customMonogram),
|
|
226
|
+
hideAttribution: pick("hide_attribution", CAPABILITY_DEFAULTS.hideAttribution),
|
|
227
|
+
topbar: pick("topbar", CAPABILITY_DEFAULTS.topbar),
|
|
228
|
+
newChat: pick("new_chat", CAPABILITY_DEFAULTS.newChat),
|
|
229
|
+
sidebar: pick("sidebar", CAPABILITY_DEFAULTS.sidebar),
|
|
230
|
+
networkBanner: pick("network_banner", CAPABILITY_DEFAULTS.networkBanner),
|
|
231
|
+
notifications: pick("notifications", CAPABILITY_DEFAULTS.notifications),
|
|
232
|
+
referral: pick("referral", CAPABILITY_DEFAULTS.referral),
|
|
233
|
+
announcements: pick("announcements", CAPABILITY_DEFAULTS.announcements),
|
|
234
|
+
appDownloadPrompt: pick("app_download_prompt", CAPABILITY_DEFAULTS.appDownloadPrompt),
|
|
235
|
+
notes: pick("notes", CAPABILITY_DEFAULTS.notes),
|
|
236
|
+
questionForum: pick("question_forum", CAPABILITY_DEFAULTS.questionForum),
|
|
237
|
+
workshopVideos: pick("workshop_videos", CAPABILITY_DEFAULTS.workshopVideos),
|
|
238
|
+
collections: pick("collections", CAPABILITY_DEFAULTS.collections),
|
|
239
|
+
ads: pick("ads", CAPABILITY_DEFAULTS.ads),
|
|
240
|
+
navBar: pick("nav_bar", CAPABILITY_DEFAULTS.navBar),
|
|
241
|
+
settings: pick("settings", CAPABILITY_DEFAULTS.settings),
|
|
242
|
+
settingsLegal: pick("settings_legal", CAPABILITY_DEFAULTS.settingsLegal),
|
|
243
|
+
settingsTheme: pick("settings_theme", CAPABILITY_DEFAULTS.settingsTheme),
|
|
244
|
+
settingsSupport: pick("settings_support", CAPABILITY_DEFAULTS.settingsSupport),
|
|
245
|
+
settingsSystemInfo: pick("settings_system_info", CAPABILITY_DEFAULTS.settingsSystemInfo),
|
|
246
|
+
settingsLogout: pick("settings_logout", CAPABILITY_DEFAULTS.settingsLogout),
|
|
247
|
+
profileAccount: pick("profile_account", CAPABILITY_DEFAULTS.profileAccount),
|
|
248
|
+
profileDeleteAccount: pick("profile_delete_account", CAPABILITY_DEFAULTS.profileDeleteAccount),
|
|
249
|
+
quickAnswer: pick("quick_answer", CAPABILITY_DEFAULTS.quickAnswer),
|
|
250
|
+
detailedAnswer: pick("detailed_answer", CAPABILITY_DEFAULTS.detailedAnswer),
|
|
251
|
+
japaneseAnswer: pick("japanese_answer", CAPABILITY_DEFAULTS.japaneseAnswer),
|
|
252
|
+
dictation: pick("dictation", CAPABILITY_DEFAULTS.dictation),
|
|
253
|
+
fileUpload: pick("file_upload", CAPABILITY_DEFAULTS.fileUpload),
|
|
254
|
+
clipboard: pick("clipboard", CAPABILITY_DEFAULTS.clipboard),
|
|
255
|
+
richClipboard: pick("rich_clipboard", CAPABILITY_DEFAULTS.richClipboard),
|
|
256
|
+
share: pick("share", CAPABILITY_DEFAULTS.share),
|
|
257
|
+
downloadToDevice: pick("download_to_device", CAPABILITY_DEFAULTS.downloadToDevice),
|
|
258
|
+
closeButton: pick("close_button", CAPABILITY_DEFAULTS.closeButton),
|
|
259
|
+
downloadPdf: pick("download_pdf", CAPABILITY_DEFAULTS.downloadPdf),
|
|
260
|
+
chatHistory: pick("chat_history", CAPABILITY_DEFAULTS.chatHistory),
|
|
261
|
+
bookmarks: pick("bookmarks", CAPABILITY_DEFAULTS.bookmarks),
|
|
262
|
+
citationsPanel: pick("citations_panel", CAPABILITY_DEFAULTS.citationsPanel),
|
|
263
|
+
referencesShowMore: pick("references_show_more", CAPABILITY_DEFAULTS.referencesShowMore),
|
|
264
|
+
directLink: pick("direct_link", CAPABILITY_DEFAULTS.directLink)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const CLIENT_BY_PLATFORM = {
|
|
269
|
+
ios: "sdk-ios",
|
|
270
|
+
android: "sdk-android"
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export async function exchange() {
|
|
274
|
+
emit("exchange.started");
|
|
275
|
+
const assertion = await fetchAssertion();
|
|
276
|
+
const attestation = await fetchAttestation(assertion);
|
|
277
|
+
const client = CLIENT_BY_PLATFORM[Platform.OS] ?? "sdk-ios";
|
|
278
|
+
const headers = {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
"X-MedGen-Client": `rn/${SDK_VERSION}`
|
|
281
|
+
};
|
|
282
|
+
if (attestation) headers["X-MedGen-Attestation"] = attestation;
|
|
283
|
+
let res;
|
|
284
|
+
try {
|
|
285
|
+
res = await fetch(`${apiBase()}/api/v1/partner/session/exchange`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: headers,
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
assertion: assertion,
|
|
290
|
+
client: client
|
|
291
|
+
})
|
|
292
|
+
});
|
|
293
|
+
} catch (e) {
|
|
294
|
+
const err = new MedGenError("NETWORK_UNAVAILABLE", e instanceof Error ? e.message : "Network unavailable", true);
|
|
295
|
+
emit("exchange.failed", {
|
|
296
|
+
code: err.code
|
|
297
|
+
});
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
const body = await res.json().catch(() => ({}));
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const err = new MedGenError(body.error ?? "UNKNOWN", body.message ?? "Exchange failed", Boolean(body.retryable), body.support_reference);
|
|
303
|
+
emit("exchange.failed", {
|
|
304
|
+
code: err.code,
|
|
305
|
+
status: res.status
|
|
306
|
+
});
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
const access = body.access;
|
|
310
|
+
const refresh = body.refresh;
|
|
311
|
+
if (typeof access !== "string" || !access || typeof refresh !== "string" || !refresh) {
|
|
312
|
+
const err = new MedGenError("UNKNOWN", "Exchange response is missing session tokens", true);
|
|
313
|
+
emit("exchange.failed", {
|
|
314
|
+
code: err.code
|
|
315
|
+
});
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
if (typeof body.handoff_code !== "string" || !body.handoff_code) {
|
|
319
|
+
const err = new MedGenError("UNKNOWN", "Exchange response is missing a handoff code", true);
|
|
320
|
+
emit("exchange.failed", {
|
|
321
|
+
code: err.code
|
|
322
|
+
});
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
await storeCredentials({
|
|
326
|
+
access: access,
|
|
327
|
+
refresh: refresh
|
|
328
|
+
});
|
|
329
|
+
const user = body.user ?? {};
|
|
330
|
+
const partner = body.partner ?? {};
|
|
331
|
+
const result = {
|
|
332
|
+
handoffCode: body.handoff_code,
|
|
333
|
+
isNewUser: Boolean(user.is_new),
|
|
334
|
+
emailCollisionPending: body.error === "EMAIL_COLLISION_PENDING",
|
|
335
|
+
brandingVersion: String(partner.branding_version ?? "1"),
|
|
336
|
+
branding: parseBranding(body.branding),
|
|
337
|
+
capabilities: parseCapabilities(partner.capabilities ?? body.capabilities)
|
|
338
|
+
};
|
|
339
|
+
emit("exchange.succeeded", {
|
|
340
|
+
isNewUser: result.isNewUser,
|
|
341
|
+
emailCollisionPending: result.emailCollisionPending
|
|
342
|
+
});
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
347
|
+
|
|
348
|
+
function toBase64(input) {
|
|
349
|
+
const bytes = [];
|
|
350
|
+
const utf8 = encodeURIComponent(input);
|
|
351
|
+
for (let i = 0; i < utf8.length; i += 1) {
|
|
352
|
+
if (utf8[i] === "%") {
|
|
353
|
+
bytes.push(parseInt(utf8.substr(i + 1, 2), 16));
|
|
354
|
+
i += 2;
|
|
355
|
+
} else {
|
|
356
|
+
bytes.push(utf8.charCodeAt(i));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
let out = "";
|
|
360
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
361
|
+
const b0 = bytes[i];
|
|
362
|
+
const b1 = bytes[i + 1];
|
|
363
|
+
const b2 = bytes[i + 2];
|
|
364
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
365
|
+
out += B64_ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
|
|
366
|
+
out += b1 === undefined ? "=" : B64_ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
|
|
367
|
+
out += b2 === undefined ? "=" : B64_ALPHABET[b2 & 63];
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function buildEmbedUrl(handoffCode, entryQuestion, capabilities) {
|
|
373
|
+
const cfg = requireConfig();
|
|
374
|
+
const params = [ [ "hc", handoffCode ], [ "locale", cfg.locale ?? "ja" ] ];
|
|
375
|
+
if (cfg.theme && capabilities?.theming !== false) {
|
|
376
|
+
const branding = {
|
|
377
|
+
colors: {
|
|
378
|
+
primary: cfg.theme.primary,
|
|
379
|
+
secondary: cfg.theme.secondary,
|
|
380
|
+
accent: cfg.theme.accent,
|
|
381
|
+
surface: cfg.theme.surface,
|
|
382
|
+
onSurface: cfg.theme.onSurface
|
|
383
|
+
},
|
|
384
|
+
colorsDark: cfg.theme.darkVariant
|
|
385
|
+
};
|
|
386
|
+
params.push([ "theme", toBase64(JSON.stringify(branding)) ]);
|
|
387
|
+
}
|
|
388
|
+
if (entryQuestion) params.push([ "q", entryQuestion ]);
|
|
389
|
+
const query = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
390
|
+
return `${embedBase()}/embed?${query}`;
|
|
391
|
+
}
|