@sendora/mcp 1.4.0 → 1.5.0
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/README.md +6 -0
- package/dist/errors.js +6 -2
- package/dist/generated/docs.json +1 -1
- package/dist/generated/screens/domain-records.html +662 -0
- package/dist/generated/screens/message-timeline.html +711 -0
- package/dist/screens.js +68 -0
- package/dist/server.js +8 -1
- package/dist/tools/domains.js +5 -0
- package/dist/tools/messages.js +3 -0
- package/dist/tools/send.js +5 -2
- package/dist/version.js +1 -1
- package/package.json +8 -5
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>Message timeline</title>
|
|
8
|
+
<style>
|
|
9
|
+
/*
|
|
10
|
+
* The screens' look (docs/design/oauth.md §9). Each colour, font, size and
|
|
11
|
+
* radius is the host's style variable where the host gives one, and the
|
|
12
|
+
* value Claude documents for it otherwise, in light and in dark. The host
|
|
13
|
+
* draws the border and the background, so the page paints neither. Status
|
|
14
|
+
* tints sit behind the primary text, which passes WCAG AA on each of them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--own-text-primary: #141413;
|
|
19
|
+
--own-text-secondary: #3d3d3a;
|
|
20
|
+
--own-text-inverse: #ffffff;
|
|
21
|
+
--own-background-secondary: #f5f4ed;
|
|
22
|
+
--own-background-inverse: #141413;
|
|
23
|
+
--own-background-info: #d6e4f6;
|
|
24
|
+
--own-background-success: #e9f1dc;
|
|
25
|
+
--own-background-warning: #f6eedf;
|
|
26
|
+
--own-background-danger: #f7ecec;
|
|
27
|
+
--own-border-primary: rgb(31 30 29 / 40%);
|
|
28
|
+
--own-border-tertiary: rgb(31 30 29 / 15%);
|
|
29
|
+
--own-ring-primary: rgb(20 20 19 / 70%);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:root[data-theme='dark'] {
|
|
33
|
+
--own-text-primary: #faf9f5;
|
|
34
|
+
--own-text-secondary: #c2c0b6;
|
|
35
|
+
--own-text-inverse: #141413;
|
|
36
|
+
--own-background-secondary: #262624;
|
|
37
|
+
--own-background-inverse: #faf9f5;
|
|
38
|
+
--own-background-info: #253e5f;
|
|
39
|
+
--own-background-success: #1b4614;
|
|
40
|
+
--own-background-warning: #483a0f;
|
|
41
|
+
--own-background-danger: #602a28;
|
|
42
|
+
--own-border-primary: rgb(222 220 209 / 40%);
|
|
43
|
+
--own-border-tertiary: rgb(222 220 209 / 15%);
|
|
44
|
+
--own-ring-primary: rgb(250 249 245 / 70%);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
html,
|
|
48
|
+
body {
|
|
49
|
+
margin: 0;
|
|
50
|
+
background: transparent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
padding: calc(12px + var(--inset-top, 0px)) calc(12px + var(--inset-right, 0px))
|
|
55
|
+
calc(12px + var(--inset-bottom, 0px)) calc(12px + var(--inset-left, 0px));
|
|
56
|
+
color: var(--color-text-primary, var(--own-text-primary));
|
|
57
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
58
|
+
font-size: var(--font-text-sm-size, 14px);
|
|
59
|
+
line-height: var(--font-text-sm-line-height, 1.4);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
h1 {
|
|
63
|
+
margin: 0 0 8px;
|
|
64
|
+
font-size: var(--font-heading-md-size, 16px);
|
|
65
|
+
line-height: var(--font-heading-md-line-height, 1.4);
|
|
66
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h2 {
|
|
70
|
+
margin: 0;
|
|
71
|
+
font-size: var(--font-heading-sm-size, 14px);
|
|
72
|
+
line-height: var(--font-heading-sm-line-height, 1.4);
|
|
73
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
p {
|
|
77
|
+
margin: 0 0 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ul,
|
|
81
|
+
ol {
|
|
82
|
+
margin: 0 0 12px;
|
|
83
|
+
padding: 0;
|
|
84
|
+
list-style: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.aside,
|
|
88
|
+
dt,
|
|
89
|
+
.reason,
|
|
90
|
+
time {
|
|
91
|
+
color: var(--color-text-secondary, var(--own-text-secondary));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.aside,
|
|
95
|
+
.reason {
|
|
96
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
97
|
+
line-height: var(--font-text-xs-line-height, 1.4);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.badge {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
padding: 1px 8px;
|
|
103
|
+
border-radius: var(--border-radius-full, 9999px);
|
|
104
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
105
|
+
line-height: var(--font-text-xs-line-height, 1.4);
|
|
106
|
+
font-weight: var(--font-weight-medium, 500);
|
|
107
|
+
color: var(--color-text-primary, var(--own-text-primary));
|
|
108
|
+
background: var(--color-background-secondary, var(--own-background-secondary));
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.badge[data-tone='info'],
|
|
113
|
+
.note {
|
|
114
|
+
background: var(--color-background-info, var(--own-background-info));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.badge[data-tone='success'] {
|
|
118
|
+
background: var(--color-background-success, var(--own-background-success));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.badge[data-tone='warning'] {
|
|
122
|
+
background: var(--color-background-warning, var(--own-background-warning));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.badge[data-tone='danger'],
|
|
126
|
+
.refusal {
|
|
127
|
+
background: var(--color-background-danger, var(--own-background-danger));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.note,
|
|
131
|
+
.refusal {
|
|
132
|
+
padding: 8px 12px;
|
|
133
|
+
border-radius: var(--border-radius-md, 8px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.address {
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.value {
|
|
141
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
142
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
143
|
+
overflow-wrap: anywhere;
|
|
144
|
+
-webkit-user-select: all;
|
|
145
|
+
user-select: all;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.facts {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
151
|
+
gap: 4px 12px;
|
|
152
|
+
margin: 0 0 12px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.facts dd {
|
|
156
|
+
margin: 0;
|
|
157
|
+
min-width: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.recipients li {
|
|
161
|
+
margin: 0 0 4px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.events li {
|
|
165
|
+
padding: 8px 0;
|
|
166
|
+
border-top: 1px solid var(--color-border-tertiary, var(--own-border-tertiary));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.events li time {
|
|
170
|
+
display: block;
|
|
171
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.events p {
|
|
175
|
+
margin: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.events:focus {
|
|
179
|
+
outline: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.standing .badge,
|
|
183
|
+
.recipients .badge {
|
|
184
|
+
margin-right: 4px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.record {
|
|
188
|
+
margin: 0 0 12px;
|
|
189
|
+
padding: 12px 0 0;
|
|
190
|
+
border-top: 1px solid var(--color-border-tertiary, var(--own-border-tertiary));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.record-head {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-wrap: wrap;
|
|
196
|
+
gap: 8px;
|
|
197
|
+
align-items: center;
|
|
198
|
+
margin: 0 0 8px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.actions {
|
|
202
|
+
margin: 4px 0 8px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
button {
|
|
206
|
+
min-height: 44px;
|
|
207
|
+
padding: 0 16px;
|
|
208
|
+
border-radius: var(--border-radius-md, 8px);
|
|
209
|
+
font: inherit;
|
|
210
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
button.primary {
|
|
215
|
+
border: 0;
|
|
216
|
+
color: var(--color-text-inverse, var(--own-text-inverse));
|
|
217
|
+
background: var(--color-background-inverse, var(--own-background-inverse));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
button.secondary {
|
|
221
|
+
border: 1px solid var(--color-border-primary, var(--own-border-primary));
|
|
222
|
+
color: var(--color-text-primary, var(--own-text-primary));
|
|
223
|
+
background: var(--color-background-secondary, var(--own-background-secondary));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
button[aria-disabled='true'] {
|
|
227
|
+
cursor: progress;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
button:focus-visible {
|
|
231
|
+
outline: 2px solid var(--color-ring-primary, var(--own-ring-primary));
|
|
232
|
+
outline-offset: 2px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.said {
|
|
236
|
+
margin: 0;
|
|
237
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
238
|
+
color: var(--color-text-secondary, var(--own-text-secondary));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.skeleton span {
|
|
242
|
+
display: block;
|
|
243
|
+
height: 12px;
|
|
244
|
+
margin: 0 0 10px;
|
|
245
|
+
border-radius: var(--border-radius-xs, 4px);
|
|
246
|
+
background: var(--color-background-secondary, var(--own-background-secondary));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.skeleton span:nth-child(1) {
|
|
250
|
+
width: 40%;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.skeleton span:nth-child(2) {
|
|
254
|
+
width: 75%;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.skeleton span:nth-child(3) {
|
|
258
|
+
width: 60%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.visually-hidden {
|
|
262
|
+
position: absolute;
|
|
263
|
+
width: 1px;
|
|
264
|
+
height: 1px;
|
|
265
|
+
margin: -1px;
|
|
266
|
+
padding: 0;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
clip-path: inset(50%);
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
border: 0;
|
|
271
|
+
}
|
|
272
|
+
</style>
|
|
273
|
+
</head>
|
|
274
|
+
<body>
|
|
275
|
+
<main>
|
|
276
|
+
<div id="screen" aria-busy="true"></div>
|
|
277
|
+
<p id="said" class="said" role="status"></p>
|
|
278
|
+
</main>
|
|
279
|
+
<script>
|
|
280
|
+
"use strict";
|
|
281
|
+
(() => {
|
|
282
|
+
// screens/dom.ts
|
|
283
|
+
function element(tag, className, ...children) {
|
|
284
|
+
const made = document.createElement(tag);
|
|
285
|
+
if (className !== "") {
|
|
286
|
+
made.className = className;
|
|
287
|
+
}
|
|
288
|
+
made.append(...children);
|
|
289
|
+
return made;
|
|
290
|
+
}
|
|
291
|
+
function badge(words, tone) {
|
|
292
|
+
const pill = element("span", "badge", words);
|
|
293
|
+
pill.dataset.tone = tone;
|
|
294
|
+
return pill;
|
|
295
|
+
}
|
|
296
|
+
function value(text) {
|
|
297
|
+
return element("code", "value", text);
|
|
298
|
+
}
|
|
299
|
+
function knownZone(zone) {
|
|
300
|
+
if (typeof zone !== "string") {
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
new Intl.DateTimeFormat("sv-SE", { timeZone: zone });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error instanceof RangeError) {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
return zone;
|
|
312
|
+
}
|
|
313
|
+
function timeOf(iso, zone) {
|
|
314
|
+
const at = new Date(iso);
|
|
315
|
+
const clock = new Intl.DateTimeFormat("sv-SE", {
|
|
316
|
+
dateStyle: "short",
|
|
317
|
+
timeStyle: "medium",
|
|
318
|
+
...zone === void 0 ? {} : { timeZone: zone }
|
|
319
|
+
});
|
|
320
|
+
const time = element("time", "", Number.isNaN(at.getTime()) ? iso : clock.format(at));
|
|
321
|
+
time.dateTime = iso;
|
|
322
|
+
return time;
|
|
323
|
+
}
|
|
324
|
+
function part(id) {
|
|
325
|
+
const found = document.getElementById(id);
|
|
326
|
+
if (found === null) {
|
|
327
|
+
throw new Error(`the document has no #${id}`);
|
|
328
|
+
}
|
|
329
|
+
return found;
|
|
330
|
+
}
|
|
331
|
+
function say(words) {
|
|
332
|
+
part("said").textContent = words;
|
|
333
|
+
}
|
|
334
|
+
function waiting() {
|
|
335
|
+
const lines = element(
|
|
336
|
+
"div",
|
|
337
|
+
"skeleton",
|
|
338
|
+
element("span", ""),
|
|
339
|
+
element("span", ""),
|
|
340
|
+
element("span", "")
|
|
341
|
+
);
|
|
342
|
+
lines.setAttribute("aria-hidden", "true");
|
|
343
|
+
const screen = part("screen");
|
|
344
|
+
screen.setAttribute("aria-busy", "true");
|
|
345
|
+
screen.replaceChildren(element("p", "visually-hidden", "Waiting for the answer."), lines);
|
|
346
|
+
}
|
|
347
|
+
function show(...children) {
|
|
348
|
+
const screen = part("screen");
|
|
349
|
+
screen.setAttribute("aria-busy", "false");
|
|
350
|
+
screen.replaceChildren(...children);
|
|
351
|
+
}
|
|
352
|
+
function failed(text) {
|
|
353
|
+
const said = element("p", "refusal", text === "" ? "The tool gave no reason." : text);
|
|
354
|
+
said.setAttribute("role", "alert");
|
|
355
|
+
return [element("h1", "", "The call failed"), said];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/version.ts
|
|
359
|
+
var MCP_VERSION = "1.5.0";
|
|
360
|
+
|
|
361
|
+
// screens/host.ts
|
|
362
|
+
var PROTOCOL_VERSION = "2026-01-26";
|
|
363
|
+
function isFields(value2) {
|
|
364
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
365
|
+
}
|
|
366
|
+
function itemsOf(value2) {
|
|
367
|
+
return Array.isArray(value2) ? value2 : [];
|
|
368
|
+
}
|
|
369
|
+
function toolResultOf(value2) {
|
|
370
|
+
const result = isFields(value2) ? value2 : {};
|
|
371
|
+
const text = itemsOf(result.content).map(
|
|
372
|
+
(block) => isFields(block) && block.type === "text" && typeof block.text === "string" ? block.text : ""
|
|
373
|
+
).join("");
|
|
374
|
+
return { text, structuredContent: result.structuredContent, isError: result.isError === true };
|
|
375
|
+
}
|
|
376
|
+
var root = document.documentElement;
|
|
377
|
+
function post(message) {
|
|
378
|
+
window.parent.postMessage({ jsonrpc: "2.0", ...message }, "*");
|
|
379
|
+
}
|
|
380
|
+
var pending = /* @__PURE__ */ new Map();
|
|
381
|
+
var nextId = 1;
|
|
382
|
+
function request(method, params) {
|
|
383
|
+
const id = nextId;
|
|
384
|
+
nextId += 1;
|
|
385
|
+
const answered = new Promise((resolve, reject) => {
|
|
386
|
+
pending.set(id, { resolve, reject });
|
|
387
|
+
});
|
|
388
|
+
post({ id, method, params });
|
|
389
|
+
return answered;
|
|
390
|
+
}
|
|
391
|
+
function settle(message) {
|
|
392
|
+
if (typeof message.id !== "number") {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const waiting2 = pending.get(message.id);
|
|
396
|
+
if (waiting2 === void 0) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
pending.delete(message.id);
|
|
400
|
+
if (isFields(message.error)) {
|
|
401
|
+
const said = message.error.message;
|
|
402
|
+
waiting2.reject(new Error(typeof said === "string" ? said : "The host refused the request."));
|
|
403
|
+
} else {
|
|
404
|
+
waiting2.resolve(message.result);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function answer(id, method) {
|
|
408
|
+
if (method === "ping" || method === "ui/resource-teardown") {
|
|
409
|
+
post({ id, result: {} });
|
|
410
|
+
} else {
|
|
411
|
+
post({ id, error: { code: -32601, message: "Method not found" } });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function theme(chosen) {
|
|
415
|
+
root.dataset.theme = chosen;
|
|
416
|
+
root.style.colorScheme = chosen;
|
|
417
|
+
}
|
|
418
|
+
function follow(context, host) {
|
|
419
|
+
if (context.theme === "light" || context.theme === "dark") {
|
|
420
|
+
theme(context.theme);
|
|
421
|
+
}
|
|
422
|
+
const variables = isFields(context.styles) ? context.styles.variables : void 0;
|
|
423
|
+
if (isFields(variables)) {
|
|
424
|
+
for (const [name, setting] of Object.entries(variables)) {
|
|
425
|
+
if (name.startsWith("--") && typeof setting === "string") {
|
|
426
|
+
root.style.setProperty(name, setting);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const insets = context.safeAreaInsets;
|
|
431
|
+
if (isFields(insets)) {
|
|
432
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
433
|
+
const inset = insets[side];
|
|
434
|
+
if (typeof inset === "number") {
|
|
435
|
+
root.style.setProperty(`--inset-${side}`, `${String(inset)}px`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if ("timeZone" in context) {
|
|
440
|
+
host.zone = knownZone(context.timeZone);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function reportSize() {
|
|
444
|
+
let last = "";
|
|
445
|
+
const report = () => {
|
|
446
|
+
const width = Math.ceil(window.innerWidth);
|
|
447
|
+
const height = Math.ceil(root.getBoundingClientRect().height);
|
|
448
|
+
const size = `${String(width)} ${String(height)}`;
|
|
449
|
+
if (size !== last) {
|
|
450
|
+
last = size;
|
|
451
|
+
post({ method: "ui/notifications/size-changed", params: { width, height } });
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
new ResizeObserver(report).observe(root);
|
|
455
|
+
report();
|
|
456
|
+
}
|
|
457
|
+
async function initialize(view, host) {
|
|
458
|
+
const answered = await request("ui/initialize", {
|
|
459
|
+
appInfo: { name: view.name, version: MCP_VERSION },
|
|
460
|
+
appCapabilities: { availableDisplayModes: ["inline"] },
|
|
461
|
+
protocolVersion: PROTOCOL_VERSION
|
|
462
|
+
});
|
|
463
|
+
const result = isFields(answered) ? answered : {};
|
|
464
|
+
host.callsTools = isFields(result.hostCapabilities) && isFields(result.hostCapabilities.serverTools);
|
|
465
|
+
follow(isFields(result.hostContext) ? result.hostContext : {}, host);
|
|
466
|
+
post({ method: "ui/notifications/initialized", params: {} });
|
|
467
|
+
reportSize();
|
|
468
|
+
}
|
|
469
|
+
function connect(view) {
|
|
470
|
+
const host = {
|
|
471
|
+
callsTools: false,
|
|
472
|
+
zone: void 0,
|
|
473
|
+
callTool: async (name, args) => toolResultOf(await request("tools/call", { name, arguments: args }))
|
|
474
|
+
};
|
|
475
|
+
const notifications = /* @__PURE__ */ new Map([
|
|
476
|
+
[
|
|
477
|
+
"ui/notifications/tool-result",
|
|
478
|
+
(params) => {
|
|
479
|
+
view.render(toolResultOf(params), host);
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
[
|
|
483
|
+
"ui/notifications/tool-cancelled",
|
|
484
|
+
() => {
|
|
485
|
+
view.stopped("The call was cancelled.");
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
[
|
|
489
|
+
"ui/notifications/host-context-changed",
|
|
490
|
+
(params) => {
|
|
491
|
+
follow(params, host);
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
]);
|
|
495
|
+
window.addEventListener("message", (event) => {
|
|
496
|
+
const message = event.data;
|
|
497
|
+
if (event.source !== window.parent || !isFields(message) || message.jsonrpc !== "2.0") {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (typeof message.method !== "string") {
|
|
501
|
+
settle(message);
|
|
502
|
+
} else if ("id" in message) {
|
|
503
|
+
answer(message.id, message.method);
|
|
504
|
+
} else {
|
|
505
|
+
notifications.get(message.method)?.(isFields(message.params) ? message.params : {});
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
theme(matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
509
|
+
initialize(view, host).catch((error) => {
|
|
510
|
+
const reason = error instanceof Error ? `: ${error.message}` : "";
|
|
511
|
+
view.stopped(`The host did not connect this screen${reason}.`);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// screens/message-timeline.ts
|
|
516
|
+
var LATEST = 5;
|
|
517
|
+
var statusWords = {
|
|
518
|
+
queued: "Queued",
|
|
519
|
+
delivered: "Delivered",
|
|
520
|
+
deferred: "Deferred",
|
|
521
|
+
bounced: "Bounced",
|
|
522
|
+
expired: "Expired"
|
|
523
|
+
};
|
|
524
|
+
var statusTones = {
|
|
525
|
+
queued: "neutral",
|
|
526
|
+
delivered: "success",
|
|
527
|
+
deferred: "warning",
|
|
528
|
+
bounced: "danger",
|
|
529
|
+
expired: "danger"
|
|
530
|
+
};
|
|
531
|
+
var statusOrder = ["delivered", "bounced", "expired", "deferred", "queued"];
|
|
532
|
+
var eventWords = {
|
|
533
|
+
Reception: "Taken for delivery",
|
|
534
|
+
Delivery: "Delivered",
|
|
535
|
+
TransientFailure: "Deferred, to be tried again",
|
|
536
|
+
Bounce: "Bounced",
|
|
537
|
+
OOB: "Bounce report by mail",
|
|
538
|
+
Expiration: "Expired",
|
|
539
|
+
Feedback: "Reported as spam",
|
|
540
|
+
AdminBounce: "Stopped by Sendora",
|
|
541
|
+
AdminRebind: "Rerouted",
|
|
542
|
+
DeferredInjectionRebind: "Rerouted",
|
|
543
|
+
Rejection: "Refused",
|
|
544
|
+
Delayed: "Delayed"
|
|
545
|
+
};
|
|
546
|
+
var SETTLED = /* @__PURE__ */ new Set(["delivered", "bounced", "expired"]);
|
|
547
|
+
function isMessage(content) {
|
|
548
|
+
return isFields(content) && Array.isArray(content.recipients) && Array.isArray(content.events);
|
|
549
|
+
}
|
|
550
|
+
function isSent(content) {
|
|
551
|
+
return isFields(content) && typeof content.messageId === "string" && "message" in content;
|
|
552
|
+
}
|
|
553
|
+
function headline(message) {
|
|
554
|
+
if (message.status === "failed") {
|
|
555
|
+
return "Not sent";
|
|
556
|
+
}
|
|
557
|
+
const counts = statusOrder.map((status) => ({
|
|
558
|
+
status,
|
|
559
|
+
count: message.recipients.filter((recipient) => recipient.status === status).length
|
|
560
|
+
})).filter(({ count }) => count > 0);
|
|
561
|
+
const [only] = counts;
|
|
562
|
+
if (counts.length === 1 && only !== void 0) {
|
|
563
|
+
return statusWords[only.status];
|
|
564
|
+
}
|
|
565
|
+
return counts.map(({ status, count }) => `${String(count)} ${statusWords[status].toLowerCase()}`).join(", ");
|
|
566
|
+
}
|
|
567
|
+
function recipients(message) {
|
|
568
|
+
return element(
|
|
569
|
+
"ul",
|
|
570
|
+
"recipients",
|
|
571
|
+
...message.recipients.map(
|
|
572
|
+
(recipient) => element(
|
|
573
|
+
"li",
|
|
574
|
+
"",
|
|
575
|
+
badge(statusWords[recipient.status], statusTones[recipient.status]),
|
|
576
|
+
" ",
|
|
577
|
+
element("span", "address", recipient.address),
|
|
578
|
+
recipient.kind === "to" ? "" : element("span", "aside", ` ${recipient.kind}`)
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function facts(entries) {
|
|
584
|
+
return element(
|
|
585
|
+
"dl",
|
|
586
|
+
"facts",
|
|
587
|
+
...entries.flatMap(([term, detail]) => [element("dt", "", term), element("dd", "", detail)])
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
function reasonOf(event) {
|
|
591
|
+
const code = event.code === null ? "" : String(event.code);
|
|
592
|
+
const details = event.details ?? "";
|
|
593
|
+
const said = details.startsWith(code) ? details : `${code} ${details}`.trim();
|
|
594
|
+
if (event.classification === null) {
|
|
595
|
+
return said;
|
|
596
|
+
}
|
|
597
|
+
return said === "" ? event.classification : `${said} (${event.classification})`;
|
|
598
|
+
}
|
|
599
|
+
function eventItem(event, message, host) {
|
|
600
|
+
const what = element("p", "what", element("strong", "", eventWords[event.type]));
|
|
601
|
+
if (event.recipient !== null && message.recipients.length > 1) {
|
|
602
|
+
what.append(" ", element("span", "address", event.recipient));
|
|
603
|
+
}
|
|
604
|
+
const item = element("li", "", timeOf(event.at, host.zone), what);
|
|
605
|
+
const reason = reasonOf(event);
|
|
606
|
+
if (reason !== "") {
|
|
607
|
+
item.append(element("p", "reason", reason));
|
|
608
|
+
}
|
|
609
|
+
return item;
|
|
610
|
+
}
|
|
611
|
+
function timeline(message, host) {
|
|
612
|
+
const heading = element("h2", "", "Timeline");
|
|
613
|
+
if (message.events.length === 0) {
|
|
614
|
+
return [heading, element("p", "aside", "No events yet.")];
|
|
615
|
+
}
|
|
616
|
+
const list = element("ol", "events");
|
|
617
|
+
list.tabIndex = -1;
|
|
618
|
+
const all = message.events.map((event) => eventItem(event, message, host));
|
|
619
|
+
if (all.length <= LATEST + 1) {
|
|
620
|
+
list.append(...all);
|
|
621
|
+
return [heading, list];
|
|
622
|
+
}
|
|
623
|
+
const earlier = all.length - LATEST;
|
|
624
|
+
list.append(...all.slice(earlier));
|
|
625
|
+
const more = element(
|
|
626
|
+
"button",
|
|
627
|
+
"secondary",
|
|
628
|
+
`Show ${String(earlier)} earlier ${earlier === 1 ? "event" : "events"}`
|
|
629
|
+
);
|
|
630
|
+
more.type = "button";
|
|
631
|
+
const showing = element("div", "actions", more);
|
|
632
|
+
more.addEventListener("click", () => {
|
|
633
|
+
list.replaceChildren(...all);
|
|
634
|
+
showing.remove();
|
|
635
|
+
list.focus();
|
|
636
|
+
say(`Showing all ${String(all.length)} events.`);
|
|
637
|
+
});
|
|
638
|
+
return [heading, showing, list];
|
|
639
|
+
}
|
|
640
|
+
function messageScreen(message, host, settled) {
|
|
641
|
+
const parts = [
|
|
642
|
+
element("h1", "", headline(message)),
|
|
643
|
+
recipients(message),
|
|
644
|
+
facts([
|
|
645
|
+
["Stream", value(message.streamId)],
|
|
646
|
+
["Submitted", timeOf(message.submittedAt, host.zone)],
|
|
647
|
+
["Message", value(message.messageId)]
|
|
648
|
+
])
|
|
649
|
+
];
|
|
650
|
+
if (message.status === "failed") {
|
|
651
|
+
parts.push(element("p", "note", "Sendora's mail server never took this message."));
|
|
652
|
+
} else if (!settled) {
|
|
653
|
+
parts.push(element("p", "note", "Not every recipient is settled yet. Ask again later."));
|
|
654
|
+
}
|
|
655
|
+
if (message.test) {
|
|
656
|
+
parts.push(
|
|
657
|
+
element(
|
|
658
|
+
"p",
|
|
659
|
+
"note",
|
|
660
|
+
"Sent from a test server: its events were simulated, and nobody received it."
|
|
661
|
+
)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return [...parts, ...timeline(message, host)];
|
|
665
|
+
}
|
|
666
|
+
function unreadScreen(sent, host) {
|
|
667
|
+
const parts = [
|
|
668
|
+
element("h1", "", "Accepted"),
|
|
669
|
+
element("p", "", "Sendora took the message, but its outcome could not be read yet."),
|
|
670
|
+
facts([
|
|
671
|
+
["Submitted", timeOf(sent.submittedAt, host.zone)],
|
|
672
|
+
["Message", value(sent.messageId)]
|
|
673
|
+
])
|
|
674
|
+
];
|
|
675
|
+
if (sent.test) {
|
|
676
|
+
parts.push(element("p", "note", "Sent from a test server: nobody will receive it."));
|
|
677
|
+
}
|
|
678
|
+
return parts;
|
|
679
|
+
}
|
|
680
|
+
function render(result, host) {
|
|
681
|
+
const content = result.structuredContent;
|
|
682
|
+
if (result.isError) {
|
|
683
|
+
show(...failed(result.text));
|
|
684
|
+
} else if (isMessage(content)) {
|
|
685
|
+
show(
|
|
686
|
+
...messageScreen(
|
|
687
|
+
content,
|
|
688
|
+
host,
|
|
689
|
+
content.recipients.every(({ status }) => SETTLED.has(status))
|
|
690
|
+
)
|
|
691
|
+
);
|
|
692
|
+
} else if (isSent(content)) {
|
|
693
|
+
show(
|
|
694
|
+
...content.message === null ? unreadScreen(content, host) : messageScreen(content.message, host, content.settled)
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
show(element("p", "", "This answer cannot be shown here. The conversation has it as text."));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
waiting();
|
|
701
|
+
connect({
|
|
702
|
+
name: "sendora-message-timeline",
|
|
703
|
+
render,
|
|
704
|
+
stopped: (words) => {
|
|
705
|
+
show(element("p", "", words));
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
})();
|
|
709
|
+
</script>
|
|
710
|
+
</body>
|
|
711
|
+
</html>
|