@rikaidev/yomi 0.2.6
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 +21 -0
- package/NOTICE +81 -0
- package/PRIVACY.md +39 -0
- package/README.md +659 -0
- package/dist/auth/credential-store.js +395 -0
- package/dist/auth/keychain.js +256 -0
- package/dist/cli/login.js +142 -0
- package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
- package/dist/line/auth/protocol/index.js +3 -0
- package/dist/line/auth/protocol/login-metadata.js +159 -0
- package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
- package/dist/line/auth/pwless/index.js +163 -0
- package/dist/line/auth/pwless/steps.js +213 -0
- package/dist/line/client/auth-service/client.js +240 -0
- package/dist/line/client/e2ee-service/client.js +69 -0
- package/dist/line/client/e2ee-service/requests.js +92 -0
- package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
- package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
- package/dist/line/client/index.js +213 -0
- package/dist/line/client/obs-media-client.js +140 -0
- package/dist/line/client/parsers.js +69 -0
- package/dist/line/client/shop-service/client.js +63 -0
- package/dist/line/client/sticker-meta.js +46 -0
- package/dist/line/client/sync-service/client.js +317 -0
- package/dist/line/client/talk-service/chat-query.js +122 -0
- package/dist/line/client/talk-service/command-client.js +242 -0
- package/dist/line/client/talk-service/contact-query.js +77 -0
- package/dist/line/client/talk-service/message-box-query.js +137 -0
- package/dist/line/client/talk-service/message-id-query.js +241 -0
- package/dist/line/client/talk-service/query-client.js +219 -0
- package/dist/line/client/talk-service/requests.js +383 -0
- package/dist/line/client/transport.js +138 -0
- package/dist/line/core/auth-session-logout.js +55 -0
- package/dist/line/core/auth-session-runtime.js +231 -0
- package/dist/line/core/auth-session-service.js +85 -0
- package/dist/line/core/chat-runtime-service.js +343 -0
- package/dist/line/core/client-runtime.js +17 -0
- package/dist/line/core/config.js +67 -0
- package/dist/line/core/constants.js +56 -0
- package/dist/line/core/contact-query-service.js +20 -0
- package/dist/line/core/directory-runtime-service.js +83 -0
- package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
- package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
- package/dist/line/core/e2ee/index.js +6 -0
- package/dist/line/core/e2ee/keys/decrypt.js +110 -0
- package/dist/line/core/e2ee/keys/encrypt.js +117 -0
- package/dist/line/core/e2ee/keys/group-key.js +260 -0
- package/dist/line/core/e2ee/keys/key-manager.js +231 -0
- package/dist/line/core/e2ee/keys/key-payload.js +130 -0
- package/dist/line/core/e2ee/keys/key-types.js +1 -0
- package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
- package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
- package/dist/line/core/e2ee/keys/peer-key.js +191 -0
- package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
- package/dist/line/core/e2ee/types.js +4 -0
- package/dist/line/core/group-query-service.js +20 -0
- package/dist/line/core/media-decrypt.js +78 -0
- package/dist/line/core/media-encrypt.js +97 -0
- package/dist/line/core/mention.js +89 -0
- package/dist/line/core/message-command-service.js +454 -0
- package/dist/line/core/message-query-service.js +161 -0
- package/dist/line/core/service.js +134 -0
- package/dist/line/core/session-state/index.js +243 -0
- package/dist/line/core/session-state/persist.js +192 -0
- package/dist/line/core/store.js +8 -0
- package/dist/line/core/thrift/binary/binary-reader.js +151 -0
- package/dist/line/core/thrift/binary/binary-writer.js +157 -0
- package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
- package/dist/line/core/thrift/compact/compact-reader.js +180 -0
- package/dist/line/core/thrift/compact/compact-writer.js +161 -0
- package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
- package/dist/line/core/thrift/fields/builders.js +124 -0
- package/dist/line/core/thrift/index.js +12 -0
- package/dist/line/core/thrift/message/decoder.js +55 -0
- package/dist/line/core/thrift/message/encoder.js +25 -0
- package/dist/line/core/thrift/primitives/varint.js +64 -0
- package/dist/line/core/thrift/struct/reader.js +172 -0
- package/dist/line/core/thrift/struct/writer.js +189 -0
- package/dist/line/core/thrift/types.js +23 -0
- package/dist/line/core/video-thumbnail.js +104 -0
- package/dist/mcp/handlers/contacts.js +166 -0
- package/dist/mcp/handlers/conversations.js +193 -0
- package/dist/mcp/handlers/groups.js +123 -0
- package/dist/mcp/handlers/index.js +15 -0
- package/dist/mcp/handlers/login.js +473 -0
- package/dist/mcp/handlers/media.js +69 -0
- package/dist/mcp/handlers/message-actions.js +62 -0
- package/dist/mcp/handlers/scope.js +120 -0
- package/dist/mcp/handlers/search.js +196 -0
- package/dist/mcp/handlers/send.js +523 -0
- package/dist/mcp/handlers/shared.js +84 -0
- package/dist/mcp/handlers/stickers.js +87 -0
- package/dist/mcp/media.js +228 -0
- package/dist/mcp/names.js +133 -0
- package/dist/mcp/phi-guard.js +105 -0
- package/dist/mcp/policy.js +34 -0
- package/dist/mcp/server.js +301 -0
- package/dist/mcp/tools.js +761 -0
- package/dist/mcp/ui/capability.js +26 -0
- package/dist/mcp/ui/login-app-protocol.js +118 -0
- package/dist/mcp/ui/login-app-styles.js +142 -0
- package/dist/mcp/ui/login-app-view.js +361 -0
- package/dist/mcp/ui/login-app.js +72 -0
- package/dist/mcp/ui/login-structured-content.js +1 -0
- package/dist/mcp/ui/resource.js +35 -0
- package/dist/mcp/ui/tools-with-ui.js +40 -0
- package/dist/search/bigram.js +86 -0
- package/dist/search/capture.js +140 -0
- package/dist/search/collector.js +176 -0
- package/dist/search/default-embedder.js +101 -0
- package/dist/search/embedder.js +13 -0
- package/dist/search/scope.js +142 -0
- package/dist/search/sqlite.js +141 -0
- package/dist/search/store.js +469 -0
- package/dist/util/data-dir.js +55 -0
- package/dist/util/log.js +86 -0
- package/dist/version.js +7 -0
- package/package.json +63 -0
- package/run.mjs +71 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The MCP Apps UI mime type the host must declare support for. */
|
|
2
|
+
export const MCP_APPS_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
3
|
+
/** The extension key the MCP Apps spec registers capability support under. */
|
|
4
|
+
const MCP_APPS_EXTENSION_KEY = 'io.modelcontextprotocol/ui';
|
|
5
|
+
/**
|
|
6
|
+
* Does the connected client's negotiated capabilities include MCP Apps UI
|
|
7
|
+
* support (the `text/html;profile=mcp-app` mime type under the
|
|
8
|
+
* `io.modelcontextprotocol/ui` extension)?
|
|
9
|
+
*
|
|
10
|
+
* @param capabilities - Result of `server.getClientCapabilities()`.
|
|
11
|
+
* @returns True only when the client explicitly declared the mime type.
|
|
12
|
+
*/
|
|
13
|
+
export function supportsMcpApps(capabilities) {
|
|
14
|
+
const extension = capabilities?.extensions?.[MCP_APPS_EXTENSION_KEY];
|
|
15
|
+
if (!extension || typeof extension !== 'object') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (!('mimeTypes' in extension)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const { mimeTypes } = extension;
|
|
22
|
+
if (!Array.isArray(mimeTypes)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return mimeTypes.includes(MCP_APPS_MIME_TYPE);
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC-over-postMessage transport for the login view, plus the MCP
|
|
3
|
+
* Apps lifecycle handshake (`ui/initialize` -> `ui/notifications/initialized`)
|
|
4
|
+
* and size-changed reporting. Emitted verbatim into ../login-app.ts's
|
|
5
|
+
* `<script>` tag, ahead of ./login-app-view.ts's rendering code, inside one
|
|
6
|
+
* shared `(function () { 'use strict'; ... })();` wrapper both blobs are
|
|
7
|
+
* concatenated into — so the `var`/`function` declarations here are plain
|
|
8
|
+
* ordinary bindings, visible to login-app-view.ts by simple scope, not
|
|
9
|
+
* exported through any `window.*` global. Split out of login-app.ts purely
|
|
10
|
+
* to keep that file (and this one) under the project's 500-scc-line module
|
|
11
|
+
* cap.
|
|
12
|
+
*
|
|
13
|
+
* Per the MCP Apps spec (specification/2026-01-26/apps.mdx), the JSON-RPC
|
|
14
|
+
* message IS the postMessage payload (no envelope), sent to `window.parent`
|
|
15
|
+
* with targetOrigin `'*'` — the view must not perform origin checks; it
|
|
16
|
+
* trusts `window.parent`. ONE `message` listener demultiplexes both
|
|
17
|
+
* directions: a message carrying `id` plus `result`/`error` is a response
|
|
18
|
+
* to a request THIS view sent (`sendRequest`/`callTool`); a message
|
|
19
|
+
* carrying only `method` is a notification FROM the host (`onNotification`
|
|
20
|
+
* subscribers). The spec's own reference snippet sets up one listener per
|
|
21
|
+
* outstanding request, which leaks a listener per call — this file adds
|
|
22
|
+
* exactly one, for the page's lifetime, and correlates by `id` instead.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Build the transport/handshake JS, parameterized by yomi's own version so
|
|
26
|
+
* `ui/initialize`'s `clientInfo.version` reports it.
|
|
27
|
+
*
|
|
28
|
+
* @param version - `YOMI_VERSION` from ../../version.ts, a build-time
|
|
29
|
+
* constant (not a runtime package.json read — that fails inside a `bun
|
|
30
|
+
* build --compile` binary's virtual filesystem). version.test.ts asserts
|
|
31
|
+
* it stays equal to package.json's "version" field so the two can't drift.
|
|
32
|
+
*/
|
|
33
|
+
export function buildLoginAppProtocolJs(version) {
|
|
34
|
+
const versionLiteral = JSON.stringify(version);
|
|
35
|
+
return `
|
|
36
|
+
var nextRequestId = 1;
|
|
37
|
+
var pendingRequests = {};
|
|
38
|
+
var notificationHandlers = {};
|
|
39
|
+
|
|
40
|
+
function onNotification(method, handler) {
|
|
41
|
+
notificationHandlers[method] = handler;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sendRequest(method, params) {
|
|
45
|
+
var id = nextRequestId++;
|
|
46
|
+
return new Promise(function (resolve, reject) {
|
|
47
|
+
pendingRequests[id] = { resolve: resolve, reject: reject };
|
|
48
|
+
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sendNotification(method, params) {
|
|
53
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function callTool(name, toolArguments) {
|
|
57
|
+
return sendRequest('tools/call', { name: name, arguments: toolArguments || {} });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
window.addEventListener('message', function (event) {
|
|
61
|
+
var message = event.data;
|
|
62
|
+
if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return;
|
|
63
|
+
var hasId = Object.prototype.hasOwnProperty.call(message, 'id');
|
|
64
|
+
var isResponse = hasId && (Object.prototype.hasOwnProperty.call(message, 'result')
|
|
65
|
+
|| Object.prototype.hasOwnProperty.call(message, 'error'));
|
|
66
|
+
if (isResponse) {
|
|
67
|
+
var waiting = pendingRequests[message.id];
|
|
68
|
+
if (!waiting) return;
|
|
69
|
+
delete pendingRequests[message.id];
|
|
70
|
+
if (message.error) {
|
|
71
|
+
waiting.reject(new Error((message.error && message.error.message) || 'Request failed.'));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
waiting.resolve(message.result);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (typeof message.method === 'string') {
|
|
79
|
+
var handler = notificationHandlers[message.method];
|
|
80
|
+
if (handler) handler(message.params || {});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
var lastReportedSize = null;
|
|
85
|
+
|
|
86
|
+
function reportSize() {
|
|
87
|
+
var width = document.documentElement.scrollWidth;
|
|
88
|
+
var height = document.documentElement.scrollHeight;
|
|
89
|
+
var key = width + 'x' + height;
|
|
90
|
+
if (key === lastReportedSize) return;
|
|
91
|
+
lastReportedSize = key;
|
|
92
|
+
sendNotification('ui/notifications/size-changed', { width: width, height: height });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyTheme(theme) {
|
|
96
|
+
var root = document.documentElement;
|
|
97
|
+
root.classList.remove('theme-dark', 'theme-light');
|
|
98
|
+
if (theme === 'dark') root.classList.add('theme-dark');
|
|
99
|
+
else if (theme === 'light') root.classList.add('theme-light');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function initializeHost(onReady) {
|
|
103
|
+
sendRequest('ui/initialize', {
|
|
104
|
+
capabilities: {},
|
|
105
|
+
clientInfo: { name: 'yomi login', version: ${versionLiteral} },
|
|
106
|
+
protocolVersion: '2026-01-26',
|
|
107
|
+
appCapabilities: { availableDisplayModes: ['inline'] },
|
|
108
|
+
}).then(function (result) {
|
|
109
|
+
var hostContext = (result && result.hostContext) || {};
|
|
110
|
+
applyTheme(hostContext.theme || null);
|
|
111
|
+
sendNotification('ui/notifications/initialized', {});
|
|
112
|
+
onReady(hostContext, null);
|
|
113
|
+
}).catch(function (error) {
|
|
114
|
+
onReady(null, error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS for the login view (../login-app.ts's `<style>` block). Split out
|
|
3
|
+
* purely to keep login-app.ts under the project's 500-scc-line module cap
|
|
4
|
+
* as this view grew from a read-only display into one that also renders a
|
|
5
|
+
* credentials form and drives its own tool calls.
|
|
6
|
+
*
|
|
7
|
+
* Color scheme: `prefers-color-scheme` is the fallback (kept from before
|
|
8
|
+
* this file existed), but `html.theme-dark` / `html.theme-light` — applied
|
|
9
|
+
* by ./login-app-protocol.ts from the host's `hostContext.theme` once
|
|
10
|
+
* `ui/initialize` resolves — take precedence via ordinary selector
|
|
11
|
+
* specificity (a class selector always outranks a media-qualified type
|
|
12
|
+
* selector), so an authoritative host theme always wins over the guess.
|
|
13
|
+
*/
|
|
14
|
+
export const LOGIN_APP_CSS = `
|
|
15
|
+
:root { color-scheme: light dark; }
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
html, body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
background: transparent;
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
22
|
+
color: #1c1c1e;
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
html, body { color: #ececec; }
|
|
26
|
+
}
|
|
27
|
+
html.theme-dark, html.theme-dark body { color: #ececec; }
|
|
28
|
+
html.theme-light, html.theme-light body { color: #1c1c1e; }
|
|
29
|
+
main {
|
|
30
|
+
padding: 20px 24px;
|
|
31
|
+
max-width: 420px;
|
|
32
|
+
margin: 0 auto;
|
|
33
|
+
}
|
|
34
|
+
.prereq {
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
opacity: 0.65;
|
|
37
|
+
line-height: 1.5;
|
|
38
|
+
margin: 0 0 16px;
|
|
39
|
+
padding-bottom: 12px;
|
|
40
|
+
border-bottom: 1px solid rgba(127, 127, 127, 0.18);
|
|
41
|
+
}
|
|
42
|
+
.label {
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
letter-spacing: 0.06em;
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
opacity: 0.6;
|
|
47
|
+
margin: 0 0 8px;
|
|
48
|
+
}
|
|
49
|
+
.pin {
|
|
50
|
+
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
51
|
+
font-size: 40px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
letter-spacing: 0.12em;
|
|
54
|
+
user-select: text;
|
|
55
|
+
-webkit-user-select: text;
|
|
56
|
+
padding: 14px 0;
|
|
57
|
+
margin: 0 0 20px;
|
|
58
|
+
border-radius: 12px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
background: rgba(127, 127, 127, 0.12);
|
|
61
|
+
}
|
|
62
|
+
.steps {
|
|
63
|
+
margin: 0 0 20px;
|
|
64
|
+
padding-left: 20px;
|
|
65
|
+
line-height: 1.6;
|
|
66
|
+
}
|
|
67
|
+
.steps li { margin-bottom: 6px; }
|
|
68
|
+
.status {
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
opacity: 0.85;
|
|
71
|
+
padding: 10px 14px;
|
|
72
|
+
border-radius: 10px;
|
|
73
|
+
background: rgba(127, 127, 127, 0.10);
|
|
74
|
+
margin: 0 0 12px;
|
|
75
|
+
}
|
|
76
|
+
.idle {
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
opacity: 0.55;
|
|
79
|
+
margin: 0;
|
|
80
|
+
}
|
|
81
|
+
.countdown {
|
|
82
|
+
font-variant-numeric: tabular-nums;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
.note {
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
opacity: 0.6;
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
margin: 16px 0 0;
|
|
90
|
+
}
|
|
91
|
+
.sources {
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
opacity: 0.45;
|
|
94
|
+
line-height: 1.6;
|
|
95
|
+
margin: 20px 0 0;
|
|
96
|
+
padding-top: 12px;
|
|
97
|
+
border-top: 1px solid rgba(127, 127, 127, 0.18);
|
|
98
|
+
word-break: break-all;
|
|
99
|
+
}
|
|
100
|
+
.error {
|
|
101
|
+
font-size: 14px;
|
|
102
|
+
padding: 12px 14px;
|
|
103
|
+
border-radius: 10px;
|
|
104
|
+
background: rgba(127, 127, 127, 0.14);
|
|
105
|
+
}
|
|
106
|
+
.form-field {
|
|
107
|
+
margin: 0 0 14px;
|
|
108
|
+
}
|
|
109
|
+
.form-field label {
|
|
110
|
+
display: block;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
opacity: 0.65;
|
|
113
|
+
margin: 0 0 6px;
|
|
114
|
+
}
|
|
115
|
+
.form-field input,
|
|
116
|
+
.form-field select {
|
|
117
|
+
width: 100%;
|
|
118
|
+
padding: 10px 12px;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
border: 1px solid rgba(127, 127, 127, 0.3);
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: inherit;
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
font-family: inherit;
|
|
125
|
+
}
|
|
126
|
+
button.primary {
|
|
127
|
+
width: 100%;
|
|
128
|
+
padding: 12px;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 10px;
|
|
131
|
+
background: rgba(127, 127, 127, 0.16);
|
|
132
|
+
color: inherit;
|
|
133
|
+
font-size: 14px;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
font-family: inherit;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
button.primary:disabled {
|
|
139
|
+
opacity: 0.6;
|
|
140
|
+
cursor: default;
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering + orchestration for the login view (../login-app.ts's
|
|
3
|
+
* `<script>` tag, concatenated after ./login-app-protocol.ts inside the
|
|
4
|
+
* same wrapping IIFE — see that file's header for why this can call
|
|
5
|
+
* `onNotification`/`callTool`/`reportSize`/`initializeHost` directly with
|
|
6
|
+
* no import). Split out of login-app.ts purely to keep that file (and this
|
|
7
|
+
* one) under the project's 500-scc-line module cap.
|
|
8
|
+
*
|
|
9
|
+
* Renders five states off one `stage`/outcome: `need_credentials` (a login
|
|
10
|
+
* form, submitted via `callTool('login', …)` — a HUMAN-initiated action),
|
|
11
|
+
* `pin` (a PIN was issued), `cert` (a stored certificate skipped the PIN
|
|
12
|
+
* step), `error`, and a plain cancelled state.
|
|
13
|
+
*
|
|
14
|
+
* This view has never been observed to render on any deployed MCP client —
|
|
15
|
+
* see ../login-app.ts's header for the upstream host bug tracking this. The
|
|
16
|
+
* MODEL, not this view, is always the driver of login completion
|
|
17
|
+
* (../handlers-login.ts always tells it to call `login_complete` itself).
|
|
18
|
+
* Accordingly `pin`/`cert` are display-only: they show the PIN/steps and a
|
|
19
|
+
* note that completion is in progress elsewhere, and do NOT call
|
|
20
|
+
* `login_complete` themselves — a prior version did, which raced the
|
|
21
|
+
* model's own call against this view's for the same `pendingLogin`. There
|
|
22
|
+
* is now exactly one driver.
|
|
23
|
+
*
|
|
24
|
+
* The `success` stage/renderer was NOT kept as a distinct visual (no "Signed
|
|
25
|
+
* in as …" card): this view no longer calls `login_complete` itself, so it
|
|
26
|
+
* has no business rendering that result. `interpretToolResult`'s fallback
|
|
27
|
+
* branch (a `login_complete`-shaped `{ loggedIn, mid, displayName }` JSON
|
|
28
|
+
* with no `structuredContent`) is technically still reachable — the host
|
|
29
|
+
* could in principle forward that tool-result notification even though this
|
|
30
|
+
* view never initiated the call — so it is handled, but only with a plain,
|
|
31
|
+
* non-optimistic `renderDone()` acknowledgement, never a profile card built
|
|
32
|
+
* from data this view did not ask for.
|
|
33
|
+
*
|
|
34
|
+
* The PIN and any error message are untrusted server/host data — written
|
|
35
|
+
* via `textContent`, never `innerHTML`; nothing interpolates into an HTML
|
|
36
|
+
* string anywhere below. All timing numbers arrive via `structuredContent`
|
|
37
|
+
* at render time — none are written into this source.
|
|
38
|
+
*/
|
|
39
|
+
export const LOGIN_APP_VIEW_JS = `
|
|
40
|
+
var SOURCE_A = 'help.line.me/line/IOSSecondary/?contentId=20018574&lang=zh-Hant';
|
|
41
|
+
var SOURCE_B = 'help.line.me/line/smartphone/pc?lang=zh-Hant&contentId=20000112';
|
|
42
|
+
|
|
43
|
+
var app = document.getElementById('app');
|
|
44
|
+
|
|
45
|
+
function clear(container) {
|
|
46
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
47
|
+
return container;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function el(tag, className, text) {
|
|
51
|
+
var node = document.createElement(tag);
|
|
52
|
+
if (className) node.className = className;
|
|
53
|
+
if (text !== undefined && text !== null) node.textContent = text;
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function appendPrereq(container) {
|
|
58
|
+
container.appendChild(el('p', 'prereq',
|
|
59
|
+
'Requires: on the primary phone, 設定 > 我的帳號 > 允許自其他裝置登入 must be enabled, '
|
|
60
|
+
+ 'or LINE will not offer this device a sign-in prompt at all.'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendSources(container) {
|
|
64
|
+
var sources = el('p', 'sources', null);
|
|
65
|
+
sources.appendChild(document.createTextNode('Sources: ' + SOURCE_A));
|
|
66
|
+
sources.appendChild(document.createElement('br'));
|
|
67
|
+
sources.appendChild(document.createTextNode(SOURCE_B));
|
|
68
|
+
container.appendChild(sources);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var countdownTimer = null;
|
|
72
|
+
|
|
73
|
+
function stopCountdown() {
|
|
74
|
+
if (countdownTimer) {
|
|
75
|
+
clearInterval(countdownTimer);
|
|
76
|
+
countdownTimer = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderCountdown(target, totalSeconds, onExpire) {
|
|
81
|
+
stopCountdown();
|
|
82
|
+
var deadline = Date.now() + totalSeconds * 1000;
|
|
83
|
+
function tick() {
|
|
84
|
+
var remaining = Math.max(0, Math.round((deadline - Date.now()) / 1000));
|
|
85
|
+
target.textContent = remaining + 's';
|
|
86
|
+
if (remaining <= 0) {
|
|
87
|
+
stopCountdown();
|
|
88
|
+
onExpire();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
tick();
|
|
92
|
+
countdownTimer = setInterval(tick, 500);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function afterRender() {
|
|
96
|
+
reportSize();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderIdle() {
|
|
100
|
+
stopCountdown();
|
|
101
|
+
var container = clear(app);
|
|
102
|
+
container.appendChild(el('p', 'idle', 'Waiting for the host.'));
|
|
103
|
+
afterRender();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderError(message) {
|
|
107
|
+
stopCountdown();
|
|
108
|
+
var container = clear(app);
|
|
109
|
+
container.appendChild(el('p', 'error', message || 'Login failed.'));
|
|
110
|
+
afterRender();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderCancelled() {
|
|
114
|
+
stopCountdown();
|
|
115
|
+
var container = clear(app);
|
|
116
|
+
container.appendChild(el('p', 'status', 'Login was cancelled.'));
|
|
117
|
+
afterRender();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// No dedicated "success" renderer: this view is never the driver of login
|
|
121
|
+
// completion (the model always is, per ../handlers-login.ts), so it has
|
|
122
|
+
// no business rendering a signed-in profile. If a \`login_complete\`
|
|
123
|
+
// tool-result notification ever reaches this view anyway (see
|
|
124
|
+
// interpretToolResult below), it gets this same plain acknowledgement,
|
|
125
|
+
// not a fabricated success card built from data this view did not ask for.
|
|
126
|
+
function renderDone() {
|
|
127
|
+
stopCountdown();
|
|
128
|
+
var container = clear(app);
|
|
129
|
+
container.appendChild(el('p', 'status', 'Sign-in finished. See the conversation for details.'));
|
|
130
|
+
afterRender();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderNeedCredentials(data) {
|
|
134
|
+
stopCountdown();
|
|
135
|
+
var container = clear(app);
|
|
136
|
+
|
|
137
|
+
appendPrereq(container);
|
|
138
|
+
container.appendChild(el('p', 'label', 'Sign in to LINE'));
|
|
139
|
+
|
|
140
|
+
var form = document.createElement('form');
|
|
141
|
+
|
|
142
|
+
var phoneField = el('div', 'form-field');
|
|
143
|
+
phoneField.appendChild(el('label', null, 'Phone number (E.164, e.g. +8869XXXXXXXX)'));
|
|
144
|
+
var phoneInput = document.createElement('input');
|
|
145
|
+
phoneInput.type = 'tel';
|
|
146
|
+
phoneInput.required = true;
|
|
147
|
+
phoneInput.placeholder = '+8869XXXXXXXX';
|
|
148
|
+
phoneField.appendChild(phoneInput);
|
|
149
|
+
form.appendChild(phoneField);
|
|
150
|
+
|
|
151
|
+
var regionField = el('div', 'form-field');
|
|
152
|
+
regionField.appendChild(el('label', null, 'Region'));
|
|
153
|
+
var regionSelect = document.createElement('select');
|
|
154
|
+
var regions = Array.isArray(data.regions) ? data.regions : [];
|
|
155
|
+
for (var i = 0; i < regions.length; i++) {
|
|
156
|
+
var option = document.createElement('option');
|
|
157
|
+
option.value = regions[i];
|
|
158
|
+
option.textContent = regions[i];
|
|
159
|
+
regionSelect.appendChild(option);
|
|
160
|
+
}
|
|
161
|
+
regionField.appendChild(regionSelect);
|
|
162
|
+
form.appendChild(regionField);
|
|
163
|
+
|
|
164
|
+
var submitButton = document.createElement('button');
|
|
165
|
+
submitButton.type = 'submit';
|
|
166
|
+
submitButton.className = 'primary';
|
|
167
|
+
submitButton.textContent = 'Continue';
|
|
168
|
+
form.appendChild(submitButton);
|
|
169
|
+
container.appendChild(form);
|
|
170
|
+
|
|
171
|
+
var formStatus = el('p', 'status', null);
|
|
172
|
+
formStatus.style.display = 'none';
|
|
173
|
+
container.appendChild(formStatus);
|
|
174
|
+
|
|
175
|
+
form.addEventListener('submit', function (event) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
submitButton.disabled = true;
|
|
178
|
+
formStatus.style.display = '';
|
|
179
|
+
formStatus.textContent = 'Starting login…';
|
|
180
|
+
callTool('login', { phone: phoneInput.value, region: regionSelect.value }).then(function (result) {
|
|
181
|
+
interpretToolResult(result);
|
|
182
|
+
}).catch(function (error) {
|
|
183
|
+
renderError((error && error.message) || 'Login failed.');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
appendSources(container);
|
|
188
|
+
afterRender();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderPinStage(data) {
|
|
192
|
+
stopCountdown();
|
|
193
|
+
var container = clear(app);
|
|
194
|
+
|
|
195
|
+
appendPrereq(container);
|
|
196
|
+
|
|
197
|
+
container.appendChild(el('p', 'label', 'LINE login PIN'));
|
|
198
|
+
container.appendChild(el('p', 'pin', data.pin || ''));
|
|
199
|
+
|
|
200
|
+
var steps = el('ol', 'steps');
|
|
201
|
+
steps.appendChild(el('li', null, 'Open LINE on your primary phone.'));
|
|
202
|
+
steps.appendChild(el('li', null, 'Enter the PIN shown above.'));
|
|
203
|
+
steps.appendChild(el('li', null, 'Tick the device that is signing in.'));
|
|
204
|
+
steps.appendChild(el('li', null, 'Tap 「用戶確認」.'));
|
|
205
|
+
steps.appendChild(el('li', null, 'Approve the new device when the primary phone asks.'));
|
|
206
|
+
container.appendChild(steps);
|
|
207
|
+
|
|
208
|
+
var pinStatus = el('p', 'status');
|
|
209
|
+
var pinLabel = document.createTextNode('Time left to enter the code: ');
|
|
210
|
+
var pinCount = el('span', 'countdown');
|
|
211
|
+
pinStatus.appendChild(pinLabel);
|
|
212
|
+
pinStatus.appendChild(pinCount);
|
|
213
|
+
container.appendChild(pinStatus);
|
|
214
|
+
|
|
215
|
+
var clientPinMinutes = Math.round(data.clientPinPollCeilingSeconds / 60);
|
|
216
|
+
container.appendChild(el('p', 'note',
|
|
217
|
+
'yomi keeps listening for about ' + clientPinMinutes + ' minutes, so a slow phone is not a '
|
|
218
|
+
+ 'problem — the code\\'s own 3-minute limit above is the real deadline.'));
|
|
219
|
+
container.appendChild(el('p', 'note',
|
|
220
|
+
'This PIN step is skipped on future logins once a login certificate has been stored.'));
|
|
221
|
+
|
|
222
|
+
var pinSeconds = data.pinCodeLifetimeSeconds;
|
|
223
|
+
renderCountdown(pinCount, pinSeconds, function () {
|
|
224
|
+
pinStatus.replaceChild(document.createTextNode('The code has expired. '), pinLabel);
|
|
225
|
+
pinCount.textContent = '';
|
|
226
|
+
container.appendChild(el('p', 'error', 'This code is no longer valid. Call login again to get a new one.'));
|
|
227
|
+
afterRender();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
container.appendChild(el('p', 'note', 'The assistant is completing this sign-in; the result will appear in the conversation.'));
|
|
231
|
+
|
|
232
|
+
appendSources(container);
|
|
233
|
+
afterRender();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderCertStage(data) {
|
|
237
|
+
stopCountdown();
|
|
238
|
+
var container = clear(app);
|
|
239
|
+
|
|
240
|
+
appendPrereq(container);
|
|
241
|
+
|
|
242
|
+
container.appendChild(el('p', 'label', 'Device approval'));
|
|
243
|
+
container.appendChild(el('p', 'status',
|
|
244
|
+
'No PIN is needed — a stored login certificate from a previous login was recognised.'));
|
|
245
|
+
|
|
246
|
+
var steps = el('ol', 'steps');
|
|
247
|
+
steps.appendChild(el('li', null, 'Open LINE on your primary phone.'));
|
|
248
|
+
steps.appendChild(el('li', null, 'Approve the new device when it asks.'));
|
|
249
|
+
container.appendChild(steps);
|
|
250
|
+
|
|
251
|
+
var approvalStatus = el('p', 'status');
|
|
252
|
+
var approvalLabel = document.createTextNode('yomi is waiting for approval: ');
|
|
253
|
+
var approvalCount = el('span', 'countdown');
|
|
254
|
+
approvalStatus.appendChild(approvalLabel);
|
|
255
|
+
approvalStatus.appendChild(approvalCount);
|
|
256
|
+
container.appendChild(approvalStatus);
|
|
257
|
+
|
|
258
|
+
var approvalSeconds = data.clientApprovalPollCeilingSeconds;
|
|
259
|
+
renderCountdown(approvalCount, approvalSeconds, function () {
|
|
260
|
+
approvalStatus.replaceChild(document.createTextNode('yomi has stopped waiting for approval. '), approvalLabel);
|
|
261
|
+
approvalCount.textContent = '';
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
container.appendChild(el('p', 'note', 'The assistant is completing this sign-in; the result will appear in the conversation.'));
|
|
265
|
+
|
|
266
|
+
appendSources(container);
|
|
267
|
+
afterRender();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractText(content) {
|
|
271
|
+
if (!Array.isArray(content)) return null;
|
|
272
|
+
for (var i = 0; i < content.length; i++) {
|
|
273
|
+
if (content[i] && content[i].type === 'text' && typeof content[i].text === 'string') {
|
|
274
|
+
return content[i].text;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseJsonText(text) {
|
|
281
|
+
if (typeof text !== 'string') return null;
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(text);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function renderFromStage(data) {
|
|
291
|
+
if (data.stage === 'need_credentials') renderNeedCredentials(data);
|
|
292
|
+
else if (data.stage === 'pin') renderPinStage(data);
|
|
293
|
+
else if (data.stage === 'cert') renderCertStage(data);
|
|
294
|
+
else renderError(typeof data.message === 'string' ? data.message : 'Login failed.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Interpret one CallToolResult-shaped object (from either a
|
|
299
|
+
* \`ui/notifications/tool-result\` notification or the direct response
|
|
300
|
+
* to this view's own \`tools/call\`) and render accordingly.
|
|
301
|
+
*/
|
|
302
|
+
function interpretToolResult(toolResult) {
|
|
303
|
+
if (!toolResult || typeof toolResult !== 'object') {
|
|
304
|
+
renderError('No result received.');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (toolResult.isError) {
|
|
308
|
+
renderError(extractText(toolResult.content) || 'The tool reported an error.');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (toolResult.structuredContent && typeof toolResult.structuredContent === 'object') {
|
|
312
|
+
renderFromStage(toolResult.structuredContent);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// login_complete's success result has no structuredContent — its JSON
|
|
316
|
+
// ({ loggedIn, mid, displayName }) is in content[0].text. This view is
|
|
317
|
+
// never the one that called login_complete (the model always is), so it
|
|
318
|
+
// only ever sees this if the host independently forwards that result —
|
|
319
|
+
// acknowledge it plainly rather than build a success card from it.
|
|
320
|
+
var parsed = parseJsonText(extractText(toolResult.content));
|
|
321
|
+
if (parsed && parsed.loggedIn) {
|
|
322
|
+
renderDone();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
renderError('Unrecognized tool result.');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A \`ui/notifications/tool-result\` notification's \`params\` may carry
|
|
330
|
+
* the CallToolResult directly, or nested under a \`result\`/\`toolResult\`/
|
|
331
|
+
* \`output\` wrapper — the spec does not pin down one exact shape, so this
|
|
332
|
+
* checks for either without assuming.
|
|
333
|
+
*/
|
|
334
|
+
function unwrapToolResult(params) {
|
|
335
|
+
if (!params || typeof params !== 'object') return null;
|
|
336
|
+
if (params.content || params.structuredContent || params.isError !== undefined) return params;
|
|
337
|
+
var candidates = [params.result, params.toolResult, params.output];
|
|
338
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
339
|
+
if (candidates[i] && typeof candidates[i] === 'object') return candidates[i];
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
onNotification('ui/notifications/tool-result', function (params) {
|
|
345
|
+
interpretToolResult(unwrapToolResult(params));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
onNotification('ui/notifications/tool-cancelled', function () {
|
|
349
|
+
renderCancelled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
renderIdle();
|
|
353
|
+
initializeHost(function (hostContext, error) {
|
|
354
|
+
if (error) {
|
|
355
|
+
renderError('Could not connect to host: ' + ((error && error.message) || String(error)));
|
|
356
|
+
}
|
|
357
|
+
// Otherwise stay idle — the next thing to arrive is
|
|
358
|
+
// ui/notifications/tool-input(-partial) (ignored, carries only the call's
|
|
359
|
+
// arguments) followed by tool-result, which drives the first real render.
|
|
360
|
+
});
|
|
361
|
+
`;
|