@bkazemi/verily 0.1.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +503 -0
  3. package/dist/browser/badge.d.ts +39 -0
  4. package/dist/browser/badge.js +440 -0
  5. package/dist/browser/connect-dialog.d.ts +83 -0
  6. package/dist/browser/connect-dialog.js +514 -0
  7. package/dist/browser/evidence-dialog.d.ts +59 -0
  8. package/dist/browser/evidence-dialog.js +541 -0
  9. package/dist/browser/index.d.ts +59 -0
  10. package/dist/browser/index.js +809 -0
  11. package/dist/browser/logo.d.ts +2 -0
  12. package/dist/browser/logo.js +17 -0
  13. package/dist/browser/mark.d.ts +19 -0
  14. package/dist/browser/mark.js +77 -0
  15. package/dist/browser/provider-mark.d.ts +6 -0
  16. package/dist/browser/provider-mark.js +99 -0
  17. package/dist/browser/signed.d.ts +40 -0
  18. package/dist/browser/signed.js +219 -0
  19. package/dist/browser.d.ts +3 -0
  20. package/dist/browser.js +3 -0
  21. package/dist/core/index.d.ts +454 -0
  22. package/dist/core/index.js +128 -0
  23. package/dist/core/signed.d.ts +49 -0
  24. package/dist/core/signed.js +113 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +5 -0
  27. package/dist/logo.d.ts +21 -0
  28. package/dist/logo.js +38 -0
  29. package/dist/postgres/index.d.ts +12 -0
  30. package/dist/postgres/index.js +76 -0
  31. package/dist/server/addresses.d.ts +2 -0
  32. package/dist/server/addresses.js +61 -0
  33. package/dist/server/body.d.ts +11 -0
  34. package/dist/server/body.js +49 -0
  35. package/dist/server/copy.d.ts +8 -0
  36. package/dist/server/copy.js +42 -0
  37. package/dist/server/discord.d.ts +7 -0
  38. package/dist/server/discord.js +69 -0
  39. package/dist/server/email-logo.d.ts +11 -0
  40. package/dist/server/email-logo.js +122 -0
  41. package/dist/server/email.d.ts +46 -0
  42. package/dist/server/email.js +145 -0
  43. package/dist/server/escape.d.ts +2 -0
  44. package/dist/server/escape.js +2 -0
  45. package/dist/server/github-gist.d.ts +11 -0
  46. package/dist/server/github-gist.js +68 -0
  47. package/dist/server/github.d.ts +7 -0
  48. package/dist/server/github.js +65 -0
  49. package/dist/server/index.d.ts +70 -0
  50. package/dist/server/index.js +862 -0
  51. package/dist/server/link.d.ts +64 -0
  52. package/dist/server/link.js +458 -0
  53. package/dist/server/openpgp.d.ts +49 -0
  54. package/dist/server/openpgp.js +158 -0
  55. package/dist/server/pgp.d.ts +29 -0
  56. package/dist/server/pgp.js +198 -0
  57. package/dist/server/public-fetch.d.ts +23 -0
  58. package/dist/server/public-fetch.js +62 -0
  59. package/dist/server/resend.d.ts +13 -0
  60. package/dist/server/resend.js +37 -0
  61. package/dist/server/service.d.ts +311 -0
  62. package/dist/server/service.js +1178 -0
  63. package/dist/server/signing.d.ts +16 -0
  64. package/dist/server/signing.js +56 -0
  65. package/dist/server/style.d.ts +17 -0
  66. package/dist/server/style.js +244 -0
  67. package/dist/server/wkd.d.ts +7 -0
  68. package/dist/server/wkd.js +65 -0
  69. package/dist/server/youtube.d.ts +11 -0
  70. package/dist/server/youtube.js +99 -0
  71. package/dist/site/index.d.ts +139 -0
  72. package/dist/site/index.js +239 -0
  73. package/dist/verily.js +11907 -0
  74. package/dist/version.d.ts +10 -0
  75. package/dist/version.js +10 -0
  76. package/docs/licenses/gpl-3.0.txt +232 -0
  77. package/docs/licenses/octicons.txt +21 -0
  78. package/docs/licenses/openpgp.txt +172 -0
  79. package/docs/licenses/rubik.txt +93 -0
  80. package/docs/licenses/simple-icons.txt +8 -0
  81. package/package.json +86 -0
@@ -0,0 +1,440 @@
1
+ import { externalName, statusLabel } from '../core/index.js';
2
+ import { clockMark, dashMark, markStyles, paintMark, verificationMark } from './mark.js';
3
+ import { providerMark } from './provider-mark.js';
4
+ const styles = `
5
+ :host { display: inline-block; max-width: 100%; vertical-align: middle; }
6
+ * { box-sizing: border-box; }
7
+ .badge, .peek {
8
+ border: 1px solid var(--verily-border, #dce2e0); border-radius: 6px;
9
+ background: var(--verily-surface, #fff); color: var(--verily-text, #202c29);
10
+ font: var(--verily-font-size, 13px)/1.35
11
+ var(--verily-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
12
+ }
13
+ .badge {
14
+ display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 3px 5px;
15
+ text-decoration: none;
16
+ }
17
+ button.badge { margin: 0; cursor: pointer; }
18
+ a[href]:hover, button.badge:hover { background: var(--verily-hover, #f3f6f4); border-color: var(--verily-border, #dce2e0); }
19
+ .badge:focus-visible { outline: 2px solid #357ce5; outline-offset: 2px; }
20
+ .name { font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
21
+ .more { flex-shrink: 0; font-size: 11px; font-weight: 600; color: var(--verily-muted, #65726c); }
22
+ .label { display: none; font-size: 11px; color: var(--verily-muted, #65726c); }
23
+ .badge:hover .label, .badge:focus-within .label { display: inline; }
24
+ .icon { display: grid; place-items: center; flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%; font-size: 12px; font-weight: 750; }
25
+ .icon .glyph { display: block; width: 12px; height: 12px; }
26
+ .mark { display: block; flex-shrink: 0; width: 20px; height: 20px; }
27
+ .mark.pending { opacity: .45; }
28
+ /* A dim ring where the account will go; it only spins where motion is welcome. */
29
+ .spinner {
30
+ flex-shrink: 0; width: 14px; height: 14px; border-radius: 50%;
31
+ border: 2px solid currentColor; opacity: .3;
32
+ }
33
+ .divider { flex-shrink: 0; width: 1px; height: 12px; background: var(--verily-border, #dce2e0); }
34
+ /* Several accounts, one to a row. The mark stays beside them and the rule runs their height. */
35
+ .badge.stacked { align-items: stretch; padding: 5px 8px 5px 6px; text-align: left; }
36
+ .stacked .mark { align-self: center; }
37
+ .stacked .divider { height: auto; }
38
+ .rows { display: grid; gap: 4px; min-width: 0; }
39
+ .row { display: flex; align-items: center; gap: 6px; min-width: 0; }
40
+ /*
41
+ * The accounts behind a short pill's count, opened beside it. Shut unless the browser
42
+ * has it open as a popover, so one without popovers never shows it at all.
43
+ */
44
+ .peek {
45
+ display: none; position: fixed; inset: auto; margin: 0; padding: 8px 10px;
46
+ width: max-content; max-width: min(320px, calc(100vw - 16px));
47
+ border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, .14); pointer-events: none;
48
+ overflow: hidden;
49
+ }
50
+ /* A row taken out to fit the window: its own display would otherwise keep it showing. */
51
+ .row[hidden] { display: none; }
52
+ .peek:popover-open { display: block; }
53
+ .peek .via { display: block; margin-top: 6px; font-size: 11px; color: var(--verily-muted, #65726c); }
54
+ .provider { display: block; flex-shrink: 0; width: 14px; height: 14px; }
55
+ .expired .icon { color: #815a12; background: #fbefce; }
56
+ .revoked .icon, .message .icon { color: #626d69; background: #edf0ee; }
57
+ .message { color: var(--verily-muted, #65726c); }
58
+ .add { font-weight: 600; }
59
+ @media (prefers-reduced-motion: no-preference) {
60
+ a, button { transition: background .12s, border-color .12s; }
61
+ /* The mark is drawn before the answer arrives, so it resolves rather than swaps. */
62
+ .mark, .mark path { transition: opacity .18s ease, stroke .18s ease; }
63
+ .spinner { border-top-color: transparent; opacity: .4; animation: verily-spin .7s linear infinite; }
64
+ }
65
+ @keyframes verily-spin { to { transform: rotate(360deg); } }
66
+ ${markStyles}
67
+ `;
68
+ /** Built once: a re-render adopts the same sheet rather than reparsing the CSS. */
69
+ let sheet;
70
+ /** The shadow host already built inside an element, kept so refreshes reuse it. */
71
+ const frames = new WeakMap();
72
+ /** Marks the waiting pill, which shows no message of its own. */
73
+ const pendingKey = '\u0000pending';
74
+ /** What each host currently shows, so an unchanged refresh can leave it alone. */
75
+ const shown = new WeakMap();
76
+ const messages = new WeakMap();
77
+ function badgeSheet() {
78
+ if (!sheet) {
79
+ sheet = new CSSStyleSheet();
80
+ sheet.replaceSync(styles);
81
+ }
82
+ return sheet;
83
+ }
84
+ /**
85
+ * The pill to draw into, emptied and ready. Everything that survives a state change is
86
+ * kept: the shadow root and its styles, the pill box itself, and the mark inside it. A
87
+ * badge that was pending and is now verified therefore resolves in place, rather than
88
+ * being torn down and built again where the reader can see it happen.
89
+ */
90
+ function frame(element, state, button = false) {
91
+ let frame = frames.get(element);
92
+ shown.delete(element);
93
+ messages.delete(element);
94
+ if (frame?.host.parentNode !== element) {
95
+ const host = document.createElement('span');
96
+ const root = host.attachShadow({ mode: 'open' });
97
+ root.adoptedStyleSheets = [badgeSheet()];
98
+ frame = { host, root };
99
+ frames.set(element, frame);
100
+ element.replaceChildren(host);
101
+ }
102
+ const tag = state === 'message' ? 'span' : state === 'connect' || button ? 'button' : 'a';
103
+ if (!frame.pill || frame.pill.tagName.toLowerCase() !== tag) {
104
+ frame.pill = document.createElement(tag);
105
+ frame.mark = undefined;
106
+ frame.root.replaceChildren(frame.pill);
107
+ }
108
+ for (const attribute of [
109
+ 'href',
110
+ 'rel',
111
+ 'title',
112
+ 'tabindex',
113
+ 'role',
114
+ 'aria-label',
115
+ 'aria-haspopup',
116
+ ])
117
+ frame.pill.removeAttribute(attribute);
118
+ // A panel belongs to the pill it was drawn for, so it goes with what that pill showed.
119
+ frame.quiet?.();
120
+ frame.quiet = undefined;
121
+ frame.peek?.remove();
122
+ frame.peek = undefined;
123
+ frame.pill.onclick =
124
+ frame.pill.onpointerenter =
125
+ frame.pill.onpointerleave =
126
+ frame.pill.onpointerdown =
127
+ frame.pill.onfocus =
128
+ frame.pill.onblur =
129
+ frame.pill.onkeydown =
130
+ null;
131
+ frame.pill.className = `badge ${state}`;
132
+ frame.mark ??= verificationMark('pending');
133
+ // The mark stays where it is, as the pill's first child: a node taken out and put back
134
+ // is styled afresh, and its colours would jump rather than resolve. Callers append what
135
+ // follows it.
136
+ if (frame.mark.parentNode !== frame.pill)
137
+ frame.pill.replaceChildren(frame.mark);
138
+ else
139
+ while (frame.mark.nextSibling)
140
+ frame.mark.nextSibling.remove();
141
+ return { pill: frame.pill, mark: frame.mark };
142
+ }
143
+ /**
144
+ * Whether what this host was last given is still in it. A page that empties the host
145
+ * itself gets a badge drawn again rather than one skipped as already shown.
146
+ */
147
+ function intact(element) {
148
+ const frame = frames.get(element);
149
+ return frame?.host.parentNode === element && frame.pill?.parentNode === frame.root;
150
+ }
151
+ /** Whether a host is currently presenting a badge of its own. */
152
+ export function badgeShown(element) {
153
+ return intact(element) && shown.has(element);
154
+ }
155
+ /** Everything the pill draws: equal keys mean an identical pill. */
156
+ function renderKey(evidence, current, label, more, linked, rows, peek) {
157
+ return JSON.stringify([
158
+ more,
159
+ linked,
160
+ peek,
161
+ rows.map((row) => [row.provider, row.providerName, externalName(row.external)]),
162
+ evidence.provider,
163
+ evidence.providerName,
164
+ externalName(evidence.external),
165
+ evidence.evidenceUrl,
166
+ evidence.verifierName,
167
+ evidence.local.label,
168
+ evidence.status,
169
+ current,
170
+ label,
171
+ ]);
172
+ }
173
+ function span(className, text) {
174
+ const element = document.createElement('span');
175
+ element.className = className;
176
+ element.textContent = text;
177
+ return element;
178
+ }
179
+ /**
180
+ * The pill before there is anything to say in it: its own frame and mark, drawn in the
181
+ * page's text colour because no verification has been read yet, and a spinner standing in
182
+ * for the account this is about. Nothing here is replaced when the answer arrives.
183
+ */
184
+ export function renderBadgePending(element) {
185
+ if (intact(element) && messages.get(element) === pendingKey)
186
+ return;
187
+ const { pill, mark } = frame(element, 'pending');
188
+ paintMark(mark, 'pending');
189
+ pill.setAttribute('role', 'status');
190
+ pill.setAttribute('aria-label', 'Checking verification');
191
+ const divider = span('divider', '');
192
+ divider.setAttribute('aria-hidden', 'true');
193
+ const spinner = span('spinner', '');
194
+ spinner.setAttribute('aria-hidden', 'true');
195
+ pill.append(divider, spinner);
196
+ messages.set(element, pendingKey);
197
+ }
198
+ export function renderBadgeMessage(element, message) {
199
+ if (intact(element) && messages.get(element) === message)
200
+ return;
201
+ const { pill, mark } = frame(element, 'message');
202
+ paintMark(mark, 'inactive');
203
+ pill.setAttribute('tabindex', '0');
204
+ pill.setAttribute('role', 'status');
205
+ pill.setAttribute('aria-label', message);
206
+ const icon = span('icon', '');
207
+ icon.append(dashMark());
208
+ icon.setAttribute('aria-hidden', 'true');
209
+ const divider = span('divider', '');
210
+ divider.setAttribute('aria-hidden', 'true');
211
+ pill.append(divider, span('label', message), icon);
212
+ messages.set(element, message);
213
+ }
214
+ /**
215
+ * Draws the pill, or returns null when the host already shows exactly this evidence:
216
+ * a periodic refresh that changes nothing must not disturb what is on screen.
217
+ *
218
+ * `more` is how many other accounts of the same subject stand behind the one shown, said
219
+ * as a count beside it. `linked` is whether the record has a page of its own a reader can
220
+ * open: an unlisted one has none, so its pill is a button and never a link to nowhere.
221
+ *
222
+ * `rows` are those of the other accounts to name, in order. Stacked, each has a row of
223
+ * its own in the pill, which is then as tall as its rows. Otherwise the pill stays short
224
+ * and, with `peek`, names them in a panel that opens beside it while a pointer rests on it
225
+ * or the keyboard is on it. Either way the accounts past the rows are a count.
226
+ */
227
+ export function renderBadge(element, evidence, { more = 0, linked = true, rows = [], peek = false, } = {}) {
228
+ const provider = evidence.providerName;
229
+ const current = evidence.status === 'verified' && evidence.expiresAt > Date.now();
230
+ const state = current ? 'verified' : evidence.status === 'revoked' ? 'revoked' : 'expired';
231
+ const label = statusLabel(evidence, Date.now());
232
+ const key = renderKey(evidence, current, label, more, linked, rows, peek);
233
+ if (intact(element) && shown.get(element) === key)
234
+ return null;
235
+ const handle = externalName(evidence.external);
236
+ const { pill: badge, mark } = frame(element, state, !linked);
237
+ const unnamed = Math.max(more - rows.length, 0);
238
+ const named = rows.map((row) => `, ${row.providerName} ${externalName(row.external)}`).join('');
239
+ const others = `${named}${unnamed ? ` and ${unnamed} more` : ''}`;
240
+ paintMark(mark, current ? 'current' : 'inactive');
241
+ if (linked) {
242
+ badge.href = evidence.evidenceUrl;
243
+ badge.rel = 'noreferrer';
244
+ }
245
+ else
246
+ badge.type = 'button';
247
+ badge.setAttribute('aria-label', `${provider} ${handle}${others}: ${label} | via: ${evidence.verifierName} | inspect verification`);
248
+ const peeked = peek && rows.length > 0;
249
+ // The provider and handle are already visible in the pill itself. A pill with a panel
250
+ // says the rest there, and a tooltip of the browser's own would only sit on top of it.
251
+ if (!peeked)
252
+ badge.title = `${label} | via: ${evidence.verifierName} | Inspect verification for ${evidence.local.label}`;
253
+ const divider = span('divider', '');
254
+ divider.setAttribute('aria-hidden', 'true');
255
+ // A provider with no mark of its own is named instead, so the pill never drops it.
256
+ const account = (shown) => [
257
+ providerMark(shown.provider, shown.attestations.external[0].method) ??
258
+ span('name', shown.providerName),
259
+ span('name', externalName(shown.external)),
260
+ ];
261
+ /** The account shown and those named after it, one to a row, then the rest as a count. */
262
+ const list = () => {
263
+ const listed = span('rows', '');
264
+ for (const shown of [evidence, ...rows]) {
265
+ const row = span('row', '');
266
+ row.append(...account(shown));
267
+ listed.append(row);
268
+ }
269
+ if (unnamed)
270
+ listed.append(span('row more', `+${unnamed} more`));
271
+ return listed;
272
+ };
273
+ if (rows.length && !peek) {
274
+ badge.className += ' stacked';
275
+ badge.append(divider, list());
276
+ }
277
+ else {
278
+ badge.append(divider, ...account(evidence));
279
+ // The accounts behind the one shown, as a number. Each has its own card in the dialog.
280
+ if (more)
281
+ badge.append(span('more', `+${more}`));
282
+ }
283
+ if (!current) {
284
+ const icon = span('icon', '');
285
+ icon.append(state === 'expired' ? clockMark() : dashMark());
286
+ icon.setAttribute('aria-hidden', 'true');
287
+ badge.append(span('label', label), icon);
288
+ }
289
+ if (peeked) {
290
+ const panel = document.createElement('div');
291
+ panel.className = 'peek';
292
+ // The pill's own label already names every account here, so this is for the eye only.
293
+ panel.setAttribute('aria-hidden', 'true');
294
+ const listed = list();
295
+ panel.append(listed, span('via', `${label} | via: ${evidence.verifierName}`));
296
+ peeks(element, badge, panel, listed, unnamed);
297
+ }
298
+ shown.set(element, key);
299
+ return badge;
300
+ }
301
+ /**
302
+ * Shuts a host's panel and lets go of what it was listening for. For a host leaving the
303
+ * page: the browser shuts its popover then, but the wait for a scroll would stay behind.
304
+ */
305
+ export function quietBadge(element) {
306
+ frames.get(element)?.quiet?.();
307
+ }
308
+ /** How long a pointer rests on a pill before its panel opens, so passing over opens nothing. */
309
+ const peekDelayMs = 150;
310
+ /**
311
+ * Opens a pill's panel while a mouse rests on it or the keyboard is on it, and shuts it
312
+ * the moment either leaves, the pill is pressed, or the page scrolls from under it.
313
+ *
314
+ * The panel is a popover, so it is drawn above the page and no container of the host's
315
+ * can clip it or sit over it. It takes no space in the page and nothing moves when it
316
+ * opens. It cannot be pointed at: the pill is the one thing to press. A browser without
317
+ * popovers never opens it, and the pill is then the short one it always was.
318
+ */
319
+ function peeks(element, pill, panel, listed, unnamed) {
320
+ const frame = frames.get(element);
321
+ let timer;
322
+ // Asked of the panel each time and never remembered: the browser shuts a popover when
323
+ // its host leaves the page, and tells nobody.
324
+ const open = () => panel.matches(':popover-open');
325
+ // Everything is let go whether or not the panel is still open, for the same reason.
326
+ const hide = () => {
327
+ clearTimeout(timer);
328
+ timer = undefined;
329
+ globalThis.removeEventListener?.('scroll', hide, true);
330
+ try {
331
+ if (open())
332
+ panel.hidePopover();
333
+ }
334
+ catch {
335
+ // Gone from the page with its host.
336
+ }
337
+ };
338
+ const show = () => {
339
+ timer = undefined;
340
+ if (typeof panel.showPopover !== 'function' || open())
341
+ return;
342
+ try {
343
+ panel.showPopover();
344
+ }
345
+ catch {
346
+ return;
347
+ }
348
+ place(panel, pill, listed, unnamed);
349
+ globalThis.addEventListener?.('scroll', hide, true);
350
+ };
351
+ panel.setAttribute('popover', 'manual');
352
+ frame.root.append(panel);
353
+ frame.peek = panel;
354
+ frame.quiet = hide;
355
+ // A finger has nowhere to rest: touching the pill presses it.
356
+ pill.onpointerenter = (event) => {
357
+ if (event.pointerType === 'touch')
358
+ return;
359
+ clearTimeout(timer);
360
+ timer = setTimeout(show, peekDelayMs);
361
+ };
362
+ pill.onpointerleave = hide;
363
+ pill.onpointerdown = hide;
364
+ pill.onblur = hide;
365
+ // Only where the keyboard put the focus: a click focuses the pill too, and opens the details.
366
+ pill.onfocus = () => {
367
+ if (pill.matches(':focus-visible'))
368
+ show();
369
+ };
370
+ pill.onkeydown = (event) => {
371
+ if (event.key === 'Escape')
372
+ hide();
373
+ };
374
+ }
375
+ /**
376
+ * Puts an open panel just under its pill, or just over it where there is more room there,
377
+ * and keeps all of it inside the window.
378
+ *
379
+ * A panel cannot be scrolled or pointed at, so whatever of it fell outside the window
380
+ * could never be read. Where it is taller than the room it has, its last accounts are
381
+ * taken out one at a time and added to the count beneath them, until it fits or one
382
+ * account is left. Each opening starts again from every row, since the window may have
383
+ * grown since the last. Whatever still does not fit is cut off at the window's edge.
384
+ */
385
+ function place(panel, pill, listed, unnamed) {
386
+ const gap = 6;
387
+ const edge = 8;
388
+ const at = pill.getBoundingClientRect();
389
+ const below = innerHeight - edge - (at.bottom + gap);
390
+ const above = at.top - gap - edge;
391
+ const rows = [...listed.children];
392
+ const accounts = rows.filter((row) => !row.className.includes('more'));
393
+ let count = rows.find((row) => row.className.includes('more'));
394
+ let dropped = 0;
395
+ const recount = () => {
396
+ const uncounted = unnamed + dropped;
397
+ if (uncounted && !count) {
398
+ count = span('row more', '');
399
+ listed.append(count);
400
+ }
401
+ if (count) {
402
+ count.textContent = `+${uncounted} more`;
403
+ count.hidden = !uncounted;
404
+ }
405
+ };
406
+ // The last opening's limit is lifted first. Left on, it would hold the panel to the
407
+ // height that fitted then, and a panel measured under it always seems to fit.
408
+ panel.style.maxHeight = '';
409
+ for (const row of accounts)
410
+ row.hidden = false;
411
+ recount();
412
+ // Under the pill unless only the room above will take it whole or is the larger.
413
+ const under = panel.getBoundingClientRect().height <= below || below >= above;
414
+ const room = Math.max(under ? below : above, 0);
415
+ while (panel.getBoundingClientRect().height > room && dropped < accounts.length - 1) {
416
+ dropped += 1;
417
+ accounts[accounts.length - dropped].hidden = true;
418
+ recount();
419
+ }
420
+ const size = panel.getBoundingClientRect();
421
+ const height = Math.min(size.height, room);
422
+ panel.style.maxHeight = `${room}px`;
423
+ panel.style.top = `${under ? at.bottom + gap : at.top - gap - height}px`;
424
+ panel.style.left = `${Math.max(edge, Math.min(at.left, innerWidth - edge - size.width))}px`;
425
+ }
426
+ /**
427
+ * The pill that starts a connection, in the same frame as the badges it will produce. Its
428
+ * mark claims nothing yet, so it is drawn in the text colour as a pending one is.
429
+ */
430
+ export function renderConnectPill(element) {
431
+ const { pill, mark } = frame(element, 'connect');
432
+ const button = pill;
433
+ paintMark(mark, 'pending');
434
+ button.type = 'button';
435
+ const divider = span('divider', '');
436
+ divider.setAttribute('aria-hidden', 'true');
437
+ button.append(divider, span('add', 'Verify an account'));
438
+ messages.set(element, 'connect');
439
+ return button;
440
+ }
@@ -0,0 +1,83 @@
1
+ import type { ExternalAccount, Instruction } from '../core/index.js';
2
+ import type { Result } from './index.js';
3
+ /** The holder's own side, as the backend names it. Never carries the private id. */
4
+ interface Subject {
5
+ heading: string;
6
+ value: string;
7
+ profileUrl?: string;
8
+ }
9
+ export interface Methods {
10
+ siteName: string;
11
+ verifierName: string;
12
+ local: Subject;
13
+ methods: {
14
+ provider: string;
15
+ method: string;
16
+ name: string;
17
+ action: string;
18
+ }[];
19
+ }
20
+ /** One connect flow as the backend reports it, at whichever step it has reached. */
21
+ export interface FlowView {
22
+ id: string;
23
+ phase: 'pending' | 'exchanging' | 'approval' | 'complete' | 'cancelled' | 'failed';
24
+ provider: {
25
+ id: string;
26
+ name: string;
27
+ method: string;
28
+ };
29
+ authorizationUrl?: string;
30
+ instructions?: Instruction[];
31
+ artifact?: 'location' | 'document';
32
+ field?: string;
33
+ note?: string;
34
+ /**
35
+ * A mailed code: what to ask the holder for, and once a code has gone, where it went,
36
+ * whether the last one entered was wrong, and how many tries are left.
37
+ */
38
+ code?: {
39
+ field: string;
40
+ input: 'email' | 'tel';
41
+ sentTo?: string;
42
+ wrong?: boolean;
43
+ triesLeft?: number;
44
+ };
45
+ local?: Subject;
46
+ external?: ExternalAccount;
47
+ joined?: {
48
+ visibility: 'public' | 'unlisted';
49
+ };
50
+ /** What the holder may choose. Absent, from an older backend, means either. */
51
+ visibilities?: ('public' | 'unlisted')[];
52
+ /** Said beside the public choice, where the backend signs its public records. */
53
+ signedNote?: string;
54
+ reason?: string;
55
+ connectionId?: string;
56
+ }
57
+ export interface ConnectApi {
58
+ methods(): Promise<Methods>;
59
+ start(provider: string, method: string): Promise<FlowView>;
60
+ read(id: string): Promise<FlowView>;
61
+ submit(id: string, artifact: string): Promise<FlowView>;
62
+ approve(id: string, visibility: 'public' | 'unlisted', cancel: boolean): Promise<Result>;
63
+ /**
64
+ * Sends the sign-in window on its way, where going straight to the provider would not
65
+ * do. Absent, the window is pointed at the flow's `authorizationUrl`.
66
+ */
67
+ enter?(popup: Window, flow: FlowView): void;
68
+ }
69
+ /**
70
+ * Walks the holder through connecting an account without leaving the page: pick a method,
71
+ * prove it, approve it. A sign-in method still needs the provider's own page, so that one
72
+ * step runs in a small window, and the dialog picks the flow back up once it returns.
73
+ * Resolves when the dialog closes, with how the last flow ended.
74
+ */
75
+ export declare function openConnectDialog(opener: HTMLElement, api: ConnectApi,
76
+ /**
77
+ * Given when this dialog was opened from another, which it stands in place of: the
78
+ * header then has a way back, and `back` is called once this dialog has closed by it.
79
+ */
80
+ back?: () => void,
81
+ /** The provider to start on, where the holder already said which account they mean. */
82
+ provider?: string): Promise<Result>;
83
+ export {};