@bkazemi/verily 0.1.0 → 0.3.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.
@@ -1,6 +1,6 @@
1
- import { attestationLabel, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
2
- import { codeAttempts, VerilyService, Unavailable } from './service.js';
3
- import { copyScript } from './copy.js';
1
+ import { attestationLabel, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
2
+ import { artifactAttempts, codeAttempts, VerilyService, Unavailable, } from './service.js';
3
+ import { copyScript, waitScript } from './copy.js';
4
4
  import { escape } from './escape.js';
5
5
  import { logo } from '../logo.js';
6
6
  import { stylesheet, styleVersion } from './style.js';
@@ -26,7 +26,7 @@ const headers = {
26
26
  'Referrer-Policy': 'same-origin',
27
27
  'X-Robots-Tag': 'noindex, nofollow',
28
28
  'X-Content-Type-Options': 'nosniff',
29
- 'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
29
+ 'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
30
30
  };
31
31
  const html = (body, status = 200) => new Response(body, {
32
32
  status,
@@ -105,12 +105,33 @@ function methodAction(provider) {
105
105
  }
106
106
  /** What the holder hands back for an artifact method, named for what it is. */
107
107
  function artifactField(provider) {
108
+ if (provider.field)
109
+ return provider.field;
108
110
  if (provider.artifact === 'document')
109
111
  return 'Your proof';
110
112
  return provider.method === 'backlink'
111
113
  ? 'Address of the page carrying your link'
112
114
  : 'Address of your published proof';
113
115
  }
116
+ /**
117
+ * Said before the holder approves, where the method they used found another proof already
118
+ * standing: it goes on the record with this one, and they did not ask for it by name.
119
+ */
120
+ function standingNote(flow, site) {
121
+ return flow.standing?.some((a) => a.method === 'backlink')
122
+ ? `This account already links back to ${site}. Confirming records that too.`
123
+ : undefined;
124
+ }
125
+ /**
126
+ * Said above a proof that was handed back and refused, while the flow still waits for it:
127
+ * why, where the provider said why in words meant for the holder, and how many tries remain.
128
+ */
129
+ function refusedNote(flow) {
130
+ if (!flow.tries)
131
+ return undefined;
132
+ const left = artifactAttempts - flow.tries;
133
+ return `${flow.reason ? `That did not check out: ${flow.reason}.` : 'That could not be checked.'} ${left === 1 ? 'One try is' : `${left} tries are`} left.`;
134
+ }
114
135
  /** Said beside every artifact method: what the holder publishes is public by design. */
115
136
  function artifactNote(provider) {
116
137
  return provider.method === 'backlink'
@@ -224,20 +245,32 @@ function evidencePage(e, base, report, keyId) {
224
245
  card(names.provider, externalName(e.external), externalLink(e.external),
225
246
  // A mailbox's address is its name already, so there is no second identifier.
226
247
  e.external.kind === 'mailbox' ? undefined : e.external.id, externalNotes(e.attestations, names)) +
227
- times([
228
- ['Approved', e.approvedAt],
229
- ['Authenticated', e.authenticatedAt],
230
- [current ? 'Valid until' : 'Expired on', e.status === 'revoked' ? undefined : e.expiresAt],
231
- ['Revoked on', e.revokedAt],
232
- // Only a method that publishes an artifact drifts; a sign-in does not go stale.
233
- [
234
- 'Last checked',
235
- e.attestations.external[0].artifactUrl
236
- ? e.attestations.external[0].confirmedAt
237
- : undefined,
238
- ],
239
- ['Sharing link expires', e.linkExpiresAt],
240
- ]) +
248
+ times(e.status === 'retired'
249
+ ? [
250
+ ['Approved', e.approvedAt],
251
+ // Two rows, always, so a reader sees any gap between them: an account retired
252
+ // weeks after it was last proved went unproved for those weeks.
253
+ ['Last verified', lastProved(e)],
254
+ ['Retired', e.retiredAt],
255
+ ['Sharing link expires', e.linkExpiresAt],
256
+ ]
257
+ : [
258
+ ['Approved', e.approvedAt],
259
+ ['Authenticated', e.authenticatedAt],
260
+ [
261
+ current ? 'Valid until' : 'Expired on',
262
+ e.status === 'revoked' ? undefined : e.expiresAt,
263
+ ],
264
+ ['Revoked on', e.revokedAt],
265
+ // Only a method that publishes an artifact drifts; a sign-in does not go stale.
266
+ [
267
+ 'Last checked',
268
+ e.attestations.external[0].artifactUrl
269
+ ? e.attestations.external[0].confirmedAt
270
+ : undefined,
271
+ ],
272
+ ['Sharing link expires', e.linkExpiresAt],
273
+ ]) +
241
274
  `${e.linkExpiresAt ? '<p class="fine">Anyone with this link can view and forward it.</p>' : ''}
242
275
  ${e.signedUrl ? `<p class="signed" id="signed"><strong>Signed by ${escape(e.verifierName)}</strong>${keyId ? ` with OpenPGP key <a href="${escape(base)}/keys.asc">${escape(keyId)}</a>` : ''}. <a href="${escape(safeUrl(e.signedUrl))}">Download the signed record</a>, which <a href="${escape(base)}/check">can be checked</a> without this page.</p>` : ''}
243
276
  <p class="fine">This connection does not establish legal identity, trustworthiness, content authorship, or permanent ownership.</p>
@@ -254,14 +287,23 @@ const checkForm = '<form method="post"><label>Signed record <textarea name="reco
254
287
  function checkedPage(d, keyId, base) {
255
288
  const names = { site: d.siteName, provider: d.providerName };
256
289
  const local = localSide(d.local, d.siteName);
257
- return page(base, 'Signed record', `<p>${escape(d.verifierName)} signed this record. It shows what stood when it was signed, not what stands now.</p>` +
290
+ // A retired record did not stand when it was signed, and its heading says so first.
291
+ const retired = d.version === 2;
292
+ return page(base, retired ? 'Retired connection' : 'Signed record', `<p>${escape(d.verifierName)} signed this record. It shows what stood when it was signed, not what stands now.</p>` +
258
293
  card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
259
294
  card(names.provider, externalName(d.external), externalLink(d.external), d.external.kind === 'mailbox' ? undefined : d.external.id, externalNotes(d.attestations, names)) +
260
- times([
261
- ['Signed', d.issuedAt],
262
- ['Approved', d.approvedAt],
263
- ['Valid until', d.expiresAt],
264
- ]) +
295
+ times(retired
296
+ ? [
297
+ ['Signed', d.issuedAt],
298
+ ['Approved', d.approvedAt],
299
+ ['Last verified', lastProved(d)],
300
+ ['Retired', d.retiredAt],
301
+ ]
302
+ : [
303
+ ['Signed', d.issuedAt],
304
+ ['Approved', d.approvedAt],
305
+ ['Valid until', d.expiresAt],
306
+ ]) +
265
307
  `<p class="fine">Signing key <a href="${escape(base)}/keys.asc">${escape(keyId)}</a>.</p>
266
308
  <p class="fine"><a href="${escape(safeUrl(d.evidenceUrl))}">See whether this connection still stands</a></p>`);
267
309
  }
@@ -353,13 +395,24 @@ export function createVerily(options) {
353
395
  /**
354
396
  * The methods a page offers. A flow on an existing record stays in its namespace, so
355
397
  * where the holder is signed in that record narrows the list; removing a link from the
356
- * external side refuses a standing proof, which anybody can hand back.
398
+ * external side refuses a standing proof, which anybody can hand back. A retired record
399
+ * is renewed by the method it was first shown by, so that one is offered alone.
357
400
  */
358
401
  async function offer(kind, id, user, requested) {
359
402
  let namespace = requested ?? undefined;
360
- if (user && id)
361
- namespace = (await service.read(id, user)).provider;
403
+ let only;
404
+ if (user && id) {
405
+ const held = await service.read(id, user);
406
+ namespace = held.provider;
407
+ if (held.status === 'retired') {
408
+ // Who can read a retired record is frozen with the rest of it.
409
+ if (kind === 'visibility')
410
+ throw new Unavailable();
411
+ only = held.attestations.external[0].method;
412
+ }
413
+ }
362
414
  const offered = (user ? permitted(user) : service.providers).filter((p) => (namespace === undefined || p.id === namespace) &&
415
+ (only === undefined || providerMethod(p) === only) &&
363
416
  !(['revoke', 'share-revoke'].includes(kind) && isArtifactProvider(p) && p.expect));
364
417
  if (!offered.length)
365
418
  throw new Unavailable();
@@ -407,8 +460,8 @@ export function createVerily(options) {
407
460
  profileUrl: user.profileUrl,
408
461
  });
409
462
  /**
410
- * A connect flow as the in-page dialog draws it: the same steps the flow pages walk
411
- * through, as data, so the holder never has to leave the page they started on.
463
+ * A connect or renew flow as the in-page dialog draws it: the same steps the flow pages
464
+ * walk through, as data, so the holder never has to leave the page they started on.
412
465
  */
413
466
  async function flowView(flow, authorizationUrl) {
414
467
  const provider = service.providerOf(flow);
@@ -424,16 +477,23 @@ export function createVerily(options) {
424
477
  instructions: provider.instructions(flow.expect),
425
478
  artifact: provider.artifact,
426
479
  field: artifactField(provider),
480
+ input: provider.input ?? 'url',
481
+ ...((flow.artifact ?? flow.suggested)
482
+ ? { suggested: flow.artifact ?? flow.suggested }
483
+ : {}),
484
+ ...(flow.tries ? { refused: refusedNote(flow) } : {}),
427
485
  note: artifactNote(provider),
428
486
  });
429
487
  if (flow.phase === 'pending' && isCodeProvider(provider))
430
488
  Object.assign(view, { code: codeStep(provider, flow), note: codeNote });
431
489
  if (flow.phase === 'approval') {
432
490
  const joined = await service.joining(flow);
491
+ const found = standingNote(flow, service.siteOf(flow.local));
433
492
  Object.assign(view, {
434
493
  local: subject(flow.local),
435
494
  external: flow.external,
436
495
  ...(joined ? { joined: { visibility: joined.visibility } } : {}),
496
+ ...(found ? { standingNote: found } : {}),
437
497
  visibilities: visibilities(flow.local),
438
498
  ...(signs ? { signedNote } : {}),
439
499
  });
@@ -444,13 +504,21 @@ export function createVerily(options) {
444
504
  view.connectionId = flow.resultId;
445
505
  return view;
446
506
  }
447
- /** A flow the dialog may read: one this holder started, to connect an account. */
507
+ /** A flow the dialog may read: one this holder started, to connect an account or renew one. */
448
508
  async function ownFlow(request, id) {
449
509
  const flow = await service.flow(id, binding(request));
450
- if (flow.kind !== 'connect' || (await local(request)).id !== flow.local?.id)
510
+ if (!['connect', 'renew'].includes(flow.kind) || (await local(request)).id !== flow.local?.id)
451
511
  throw new Unavailable();
452
512
  return flow;
453
513
  }
514
+ /**
515
+ * What the tab a confirmation was pressed in says. It is the end of that tab's part: the
516
+ * page that asked is waiting on the flow and carries on from there, so this one offers
517
+ * nowhere to go.
518
+ */
519
+ const confirmed = (flow) => html(page(prefix, flow.phase === 'failed' ? 'Address not confirmed' : 'Address confirmed', flow.phase === 'failed'
520
+ ? `<p>${escape(flow.reason ?? 'This address could not be confirmed')}.</p>`
521
+ : '<p>You can close this tab now.</p>'));
454
522
  async function handle(request) {
455
523
  const url = new URL(request.url);
456
524
  if (url.origin !== base.origin ||
@@ -481,6 +549,14 @@ export function createVerily(options) {
481
549
  return new Response(copyScript, {
482
550
  headers: { ...headers, 'Content-Type': 'text/javascript' },
483
551
  });
552
+ if (path === '/wait.js')
553
+ return new Response(waitScript, {
554
+ headers: { ...headers, 'Content-Type': 'text/javascript' },
555
+ });
556
+ const phase = path.match(/^\/flows\/([^/]+)\/phase$/);
557
+ // All a waiting page needs to know, and only the browser that started the flow.
558
+ if (phase)
559
+ return json({ phase: (await service.flow(phase[1], binding(request))).phase });
484
560
  if (path === '/result.js')
485
561
  return new Response(`const e=document.getElementById('verily-result');if(window.opener&&e){window.opener.postMessage({type:'verily-result',outcome:e.dataset.outcome,connectionId:e.dataset.id},location.origin);window.close()}`, { headers: { ...headers, 'Content-Type': 'text/javascript' } });
486
562
  if (path === '/verify' ||
@@ -564,10 +640,11 @@ export function createVerily(options) {
564
640
  if ((await local(request)).id !== flow.local?.id)
565
641
  throw new Unavailable();
566
642
  return html(page(prefix, `Verify with ${provider.name}`, `${instructions(provider.instructions(flow.expect))}
643
+ ${flow.tries ? `<p>${escape(refusedNote(flow))}</p>` : ''}
567
644
  <form method="post" action="${escape(prefix)}/flows/${escape(flow.id)}/submit">
568
645
  <label>${escape(artifactField(provider))} ${provider.artifact === 'document'
569
- ? '<textarea name="artifact" rows="14" cols="72" required></textarea>'
570
- : '<input name="artifact" type="url" required>'}</label>
646
+ ? `<textarea name="artifact" rows="14" cols="72" required>${escape(flow.artifact ?? '')}</textarea>`
647
+ : `<input name="artifact" type="${provider.input ?? 'url'}" value="${escape(flow.artifact ?? flow.suggested ?? '')}" autocapitalize="none" spellcheck="false" required>`}</label>
571
648
  <button>Check my proof</button></form>
572
649
  <p class="fine">${escape(artifactNote(provider))}</p>
573
650
  <script src="${escape(prefix)}/copy.js" defer></script>`));
@@ -580,7 +657,7 @@ export function createVerily(options) {
580
657
  const step = codeStep(provider, flow);
581
658
  return html(page(prefix, `Verify with ${provider.name}`, `${step.sentTo === undefined
582
659
  ? ''
583
- : `<p>A message was sent to ${escape(step.sentTo)}. Press the button in it, then come back to this page.</p><p><a href="${escape(prefix)}/flows/${escape(flow.id)}">I pressed the button</a></p><p>Or enter the code from the message here.</p>${step.wrong
660
+ : `<p>A message was sent to ${escape(step.sentTo)}. Press the button in it, and this page carries on.</p><noscript><p><a href="${escape(prefix)}/flows/${escape(flow.id)}">I pressed the button</a></p></noscript><p>Or enter the code from the message here.</p>${step.wrong
584
661
  ? `<p>That code did not match. ${step.triesLeft === 1 ? 'One try is' : `${step.triesLeft} tries are`} left.</p>`
585
662
  : ''}`}
586
663
  <form method="post" action="${escape(prefix)}/flows/${escape(flow.id)}/submit">
@@ -588,7 +665,7 @@ export function createVerily(options) {
588
665
  ? `<input name="artifact" type="${step.input}" autocomplete="${step.input}" required>`
589
666
  : '<input name="artifact" autocomplete="one-time-code" autocapitalize="characters" spellcheck="false" required>'}</label>
590
667
  <button>${step.sentTo === undefined ? `Send the ${escape(provider.name.toLowerCase())}` : 'Check my code'}</button></form>
591
- ${step.sentTo === undefined ? `<p class="fine">${escape(codeNote)}</p>` : ''}`));
668
+ ${step.sentTo === undefined ? `<p class="fine">${escape(codeNote)}</p>` : `<script src="${escape(prefix)}/wait.js" defer></script>`}`));
592
669
  }
593
670
  if (flow.phase !== 'approval')
594
671
  return await ended(flow);
@@ -599,6 +676,7 @@ export function createVerily(options) {
599
676
  // visibility was chosen when it was made and is not the holder's to rechoose here.
600
677
  const joined = await service.joining(flow);
601
678
  const kept = ['revoke', 'share-revoke', 'renew'].includes(flow.kind) || joined;
679
+ const found = standingNote(flow, service.siteOf(flow.local));
602
680
  return html(page(prefix, ['revoke', 'share-revoke'].includes(flow.kind)
603
681
  ? 'Remove connection'
604
682
  : flow.kind === 'renew'
@@ -609,6 +687,7 @@ export function createVerily(options) {
609
687
  ${card(subject(flow.local).heading, subject(flow.local).value, flow.local.profileUrl)}
610
688
  ${card(provider.name, externalName(flow.external), externalLink(flow.external), flow.external.kind === 'mailbox' ? undefined : flow.external.id)}
611
689
  ${joined ? `<p>This account is already linked here. Confirming adds this method to that connection, beneath the one it was first shown by, and it stays ${escape(joined.visibility)}.</p>` : ''}
690
+ ${found ? `<p>${escape(found)}</p>` : ''}
612
691
  <p class="fine">${escape(service.siteOf(flow.local))} receives the result. Verified via ${escape(options.verifierName)}.</p>
613
692
  <form method="post" action="${escape(prefix)}/flows/${escape(flow.id)}/approve">
614
693
  ${kept ? '<input type="hidden" name="visibility" value="unlisted">' : visibilityChoice(flow.local, visibilities(flow.local), signs)}
@@ -617,11 +696,16 @@ export function createVerily(options) {
617
696
  }
618
697
  const confirmation = path.match(/^\/confirm\/([^/]+)$/);
619
698
  // Where the button in a message leads, in whatever browser the holder reads mail
620
- // in. Arriving confirms nothing: a scanner that opens every link in a message gets
621
- // this page and stops, and the holder is shown what they are confirming first.
699
+ // in. In the browser that started the flow, arriving is the confirmation: its
700
+ // cookie shows the one who asked is the one who opened the message. Anywhere else
701
+ // arriving confirms nothing: a scanner that opens every link in a message carries
702
+ // no cookie, gets this page and stops, and a holder on another device is shown
703
+ // what they are confirming first.
622
704
  if (confirmation) {
623
705
  const token = url.searchParams.get('token') ?? '';
624
706
  const asked = await service.confirming(confirmation[1], token);
707
+ if (await service.flow(confirmation[1], binding(request)).catch(() => undefined))
708
+ return confirmed(await service.confirm(confirmation[1], token));
625
709
  const next = {
626
710
  connect: `link it to this ${subjectNoun(asked.local?.kind) ?? 'site'}`,
627
711
  renew: 'renew its link',
@@ -720,22 +804,16 @@ export function createVerily(options) {
720
804
  const flow = await service.start(user, data.connectionId || undefined, kind, await choose(user, data.connectionId || undefined, {
721
805
  provider: data.provider || undefined,
722
806
  method: data.method || undefined,
723
- }), await context(request, kind));
724
- if (asJson && kind === 'connect')
807
+ }), await context(request, kind),
808
+ // Posted from this origin, which the check on every POST has already held it to.
809
+ true);
810
+ if (asJson && ['connect', 'renew'].includes(kind))
725
811
  return json(await flowView(await service.flow(flow.flowId, flow.binding), flow.authorizationUrl), flowCookie(flow.binding));
726
812
  return redirect(entry(flow), flowCookie(flow.binding));
727
813
  }
728
814
  const confirmation = path.match(/^\/confirm\/([^/]+)$/);
729
815
  if (confirmation) {
730
- const flow = await service.confirm(confirmation[1], data.token ?? '');
731
- // The flow is still the browser's that started it. This one may be that browser,
732
- // and then it can carry on from here; any other is sent back to where it began.
733
- const here = await service.flow(flow.id, binding(request)).catch(() => undefined);
734
- return html(page(prefix, flow.phase === 'failed' ? 'Address not confirmed' : 'Address confirmed', flow.phase === 'failed'
735
- ? `<p>${escape(flow.reason ?? 'This address could not be confirmed')}.</p>`
736
- : `<p>Go back to the page where you started. It carries on from here.</p>${here
737
- ? `<p><a href="${escape(prefix)}/flows/${escape(flow.id)}">Or carry on in this window</a></p>`
738
- : '<p>You can close this window.</p>'}`));
816
+ return confirmed(await service.confirm(confirmation[1], data.token ?? ''));
739
817
  }
740
818
  const submission = path.match(/^\/flows\/([^/]+)\/submit$/);
741
819
  if (submission) {
@@ -769,15 +847,21 @@ export function createVerily(options) {
769
847
  return json(id ? { outcome: 'complete', connectionId: id } : { outcome: 'cancelled' });
770
848
  return await ended(await service.flow(flow.id, binding(request)));
771
849
  }
772
- const management = path.match(/^\/connections\/([^/]+)\/(disconnect|share|share-revoke|visibility)$/);
850
+ const management = path.match(/^\/connections\/([^/]+)\/(disconnect|mark|share|share-revoke|visibility)$/);
773
851
  if (management) {
774
852
  const user = await local(request), id = management[1];
775
853
  if (management[2] === 'disconnect') {
776
854
  await service.revoke(id, user);
777
855
  return json({ ok: true });
778
856
  }
857
+ // Acts on the account the record names: every record of it this holder has.
858
+ if (management[2] === 'mark') {
859
+ await service.mark(id, user, data.as);
860
+ return json({ ok: true });
861
+ }
779
862
  if (management[2] === 'visibility') {
780
- await service.read(id, user);
863
+ if ((await service.read(id, user)).status === 'retired')
864
+ throw new Unavailable();
781
865
  return json({ url: `${service.baseUrl}/visibility/${id}` });
782
866
  }
783
867
  return json((await service.share(id, user, management[2] === 'share-revoke')) ?? { ok: true });
@@ -86,6 +86,9 @@ export function linkProvider(options) {
86
86
  }
87
87
  return account(page, profile);
88
88
  },
89
+ // A page that carries the link is at the address it is named by, so a record of one
90
+ // already says where to look.
91
+ known: (account) => account.profileUrl,
89
92
  };
90
93
  }
91
94
  /**
@@ -216,18 +219,32 @@ export function githubLinkProvider(options = {}) {
216
219
  id: 'github',
217
220
  name: 'GitHub',
218
221
  hosts: ['github.com'],
219
- // GitHub's own rule: alphanumerics and single hyphens, never leading or trailing, 39 max.
220
- profile: /^\/([A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38})$/,
222
+ profile: new RegExp(`^/(${username})$`),
221
223
  });
224
+ // A profile is at its username, so the username is all the holder needs to give. A
225
+ // leading @ is how one is written, and is not part of it.
226
+ const named = new RegExp(`^@?(${username})$`);
222
227
  return {
223
228
  ...provider,
224
229
  // GitHub writes the rel="me" itself, so the holder only fills in one field.
225
230
  instructions: (expect) => [
226
- 'Put this address in the website field of your GitHub profile, then paste your profile’s address below.',
231
+ 'Put this address in the website field of your GitHub profile, then enter your GitHub username below.',
227
232
  { code: expect },
228
233
  ],
234
+ field: 'Your GitHub username',
235
+ input: 'text',
236
+ resolve(artifact) {
237
+ const name = named.exec(artifact.trim())?.[1];
238
+ return name ? `https://github.com/${name}` : artifact;
239
+ },
240
+ // Only an account has a username. The handle of anything else is not one.
241
+ known: (account) => (account.kind ?? 'account') === 'account' && named.test(account.handle)
242
+ ? account.handle.replace(/^@/, '')
243
+ : undefined,
229
244
  };
230
245
  }
246
+ /** GitHub's own rule: alphanumerics and single hyphens, never leading or trailing, 39 max. */
247
+ const username = '[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}';
231
248
  /**
232
249
  * Whether a link points at the subject. Every key in the subject's query must carry the
233
250
  * same values in the same order on the link, since a site whose profiles differ only by a
@@ -244,7 +261,10 @@ function points(href, base, subject) {
244
261
  }
245
262
  // A subject with a fragment is one of the views a hash-routed page serves, so the
246
263
  // fragment names it and must match. A fragment on the link alone is a spot on the page.
247
- if (target.protocol !== subject.protocol ||
264
+ // A site reached over http is the same site as over https, and a profile field given a
265
+ // bare domain is often written as the former. Any other scheme is some other resource.
266
+ const scheme = target.protocol === 'http:' && subject.protocol === 'https:' ? 'https:' : target.protocol;
267
+ if (scheme !== subject.protocol ||
248
268
  target.host.toLowerCase() !== subject.host.toLowerCase() ||
249
269
  trim(target.pathname) !== trim(subject.pathname) ||
250
270
  (subject.hash !== '' && target.hash !== subject.hash))
@@ -1,4 +1,4 @@
1
- import { type Attestations, type Connection, type Evidence, type ExternalAccount, type Flow, type LocalAccount, type Provider, type SignedDocument, type SignedEvidence, type Storage, type VerifierKey, type Visibility } from '../core/index.js';
1
+ import { type Attestations, type Connection, type Evidence, type ExternalAccount, type Flow, type LocalAccount, type Mark, type Provider, type SignedDocument, type SignedEvidence, type Storage, type VerifierKey, type Visibility } from '../core/index.js';
2
2
  export declare const secret: () => string;
3
3
  export declare const hash: (value: string) => string;
4
4
  export declare class Unavailable extends Error {
@@ -64,6 +64,10 @@ export declare class VerilyService {
64
64
  * A public record that stands, signed as of now. Signed when asked for, not when
65
65
  * approved, so a signed record says the record stood on the day it was taken and none can be
66
66
  * made once it is removed.
67
+ *
68
+ * A retired record is signed too, as what it is: the durable form of the claim that the
69
+ * account was the holder's when it was last proved. It did not stand when it was signed,
70
+ * so it is a version of its own that says so, which a reader of the first refuses.
67
71
  */
68
72
  signed(id: string): Promise<SignedEvidence>;
69
73
  /** What a signed record says, if this instance signed it. */
@@ -84,7 +88,13 @@ export declare class VerilyService {
84
88
  start(local?: LocalAccount, connectionId?: string, kind?: Flow['kind'], choice?: {
85
89
  provider?: string;
86
90
  method?: string;
87
- }, context?: Record<string, string>): Promise<{
91
+ }, context?: Record<string, string>,
92
+ /**
93
+ * Whether the caller has shown the holder asked for this themselves, as a form posted
94
+ * from their own page shows and following a link does not. Without it the flow reads
95
+ * nothing at its start and ends on an approval, however little there is to approve.
96
+ */
97
+ attended?: boolean): Promise<{
88
98
  flowId: string;
89
99
  binding: string;
90
100
  expect: string;
@@ -103,6 +113,19 @@ export declare class VerilyService {
103
113
  instructions?: undefined;
104
114
  authorizationUrl?: undefined;
105
115
  }>;
116
+ /**
117
+ * What a new flow can offer the holder to hand back: the one account this subject has
118
+ * already shown some other way, which is the record the flow would join. With several,
119
+ * which one they mean is theirs to say.
120
+ */
121
+ private known;
122
+ /**
123
+ * Reads the proof at the account a record already names, before the holder is asked for
124
+ * anything. Found, the flow goes straight to approval, as if they had handed it back.
125
+ * Not found is no failure: they have not been told what to publish yet, so the flow
126
+ * stays where it was and tells them.
127
+ */
128
+ private offered;
106
129
  /**
107
130
  * Which method a flow runs. A flow on an existing record stays in that record's
108
131
  * namespace, and without a choice it uses the method the record was first shown by.
@@ -110,6 +133,10 @@ export declare class VerilyService {
110
133
  * Removing a link from the external side takes a method whose proof is fresh. A standing
111
134
  * proof, such as a link back, is there for anyone to point at: handing one back shows
112
135
  * the link exists, not that whoever handed it back holds the account.
136
+ *
137
+ * A retired record is renewed by the method it was first shown by and no other. Another
138
+ * would be listed beneath a main proof nobody has read since, and the record would come
139
+ * back unconfirmed.
113
140
  */
114
141
  private chosen;
115
142
  /**
@@ -119,6 +146,13 @@ export declare class VerilyService {
119
146
  * code that arrived there.
120
147
  */
121
148
  submit(id: string, binding: string, artifact: string): Promise<string>;
149
+ /**
150
+ * Takes a refused proof as something the holder can put right, and leaves the flow
151
+ * waiting for it. What they must publish is minted per flow, so a flow ended here would
152
+ * have them publish or sign all over again for a mistyped address. The tries are counted
153
+ * all the same: each one is a fetch somewhere the holder chose.
154
+ */
155
+ private refused;
122
156
  /**
123
157
  * One step of a mailed code. The first answer is an address: a code is minted, sent
124
158
  * there, and only its hash kept. The second is the code, which the holder could only
@@ -168,8 +202,18 @@ export declare class VerilyService {
168
202
  */
169
203
  private established;
170
204
  /**
171
- * A failed check leaves the flow dead rather than retryable in place. Only a `Refused`
172
- * reason is kept: any other error may describe this backend rather than the proof.
205
+ * The proofs already standing for the account a flow just showed, by the other methods
206
+ * configured for its provider that can tell where to look from the account alone. One
207
+ * that is not there, or cannot be read, is simply not found: the holder asked for none
208
+ * of them, so none of them can fail the flow.
209
+ */
210
+ private standing;
211
+ /** Adds the proofs a flow found standing beneath the method the record was first shown by. */
212
+ private recordStanding;
213
+ /**
214
+ * A sign-in or a mailed code that fails leaves the flow dead: there is nothing in it to
215
+ * put right, and starting again costs the holder nothing. Only a `Refused` reason is
216
+ * kept: any other error may describe this backend rather than the proof.
173
217
  */
174
218
  private failed;
175
219
  approve(id: string, binding: string, local: LocalAccount | undefined, visibility: Visibility, cancel?: boolean,
@@ -179,6 +223,15 @@ export declare class VerilyService {
179
223
  * this approval makes a record or joins one, so no change in between gets past it.
180
224
  */
181
225
  allowed?: Visibility[]): Promise<string | undefined>;
226
+ /**
227
+ * Whether approving a flow would decide anything. A renewal, or another way of showing
228
+ * an account the subject is already linked to, changes neither who is linked nor who
229
+ * can read it: the holder asked for exactly this and the account has been checked
230
+ * against the record, so it is recorded without asking them to say so again.
231
+ */
232
+ private settled;
233
+ /** Records what an approved flow does, and ends it. The caller has checked who approves. */
234
+ private conclude;
182
235
  /**
183
236
  * Ends a flow and records how, inside the transaction that ends it: the connection as it
184
237
  * stands now, since whatever later reports this result must not read a record that has
@@ -198,7 +251,8 @@ export declare class VerilyService {
198
251
  * The live record a connect flow reproves by another method, if there is one: the same
199
252
  * subject linked to the same account on the same provider, first shown some other way.
200
253
  * The same method again is a second record, as it always was; renewing is how that
201
- * one is extended.
254
+ * one is extended. A retired record is never joined: it is frozen, so proving its account
255
+ * another way makes a new record and leaves that one as history behind it.
202
256
  */
203
257
  private joins;
204
258
  /**
@@ -244,6 +298,28 @@ export declare class VerilyService {
244
298
  mine(local: LocalAccount): Promise<Evidence[]>;
245
299
  private owned;
246
300
  revoke(id: string, local: LocalAccount): Promise<void>;
301
+ /**
302
+ * The holder's records of one account that are not revoked. A mark belongs to an account,
303
+ * and an account can stand on several records: shown a second way on a record of its own,
304
+ * or removed once and connected again. They are told apart as a badge tells them, by
305
+ * provider and the id the provider gave.
306
+ */
307
+ private account;
308
+ /**
309
+ * The mark an account's records carry, for a record about to stand beside them: every
310
+ * one neither revoked nor retired, lapsed ones included, since those hold a mark that
311
+ * must survive their own renewal.
312
+ */
313
+ private accountMark;
314
+ /**
315
+ * Marks an account as the holder would have it listed, or retires it. Names one record
316
+ * and acts on the account, or a mark on one record could vanish behind a sibling, and
317
+ * retiring one could leave the account reading as verified through another.
318
+ *
319
+ * No fresh proof is asked for. A mark never widens who can read a record or makes one
320
+ * read as verified, and a holder retiring an account often can no longer prove it.
321
+ */
322
+ mark(id: string, local: LocalAccount, as: 'preferred' | 'current' | 'unused' | 'retired'): Promise<void>;
247
323
  share(id: string, local: LocalAccount, revoke?: boolean): Promise<{
248
324
  url: string;
249
325
  expiresAt: number;
@@ -267,6 +343,8 @@ export declare class VerilyService {
267
343
  visibilityApprovedAt: number;
268
344
  expiresAt: number;
269
345
  revokedAt?: number;
346
+ mark?: Mark;
347
+ retiredAt?: number;
270
348
  signedUrl?: string;
271
349
  }>;
272
350
  /**
@@ -289,6 +367,8 @@ export declare class VerilyService {
289
367
  * `budget` bounds the reads per run, since providers rate-limit and the alarm this runs
290
368
  * on is shared. Oldest first, so nothing starves however many connections are waiting.
291
369
  *
370
+ * A retired record is never asked about: its proof is no longer expected to be there.
371
+ *
292
372
  * Returns how many were confirmed. A connection revoked here is not one of them.
293
373
  */
294
374
  recheck(budget?: number): Promise<number>;
@@ -302,10 +382,16 @@ export declare class VerilyService {
302
382
  private reread;
303
383
  /** A provider that never answers must not hold up the maintenance run behind it. */
304
384
  private deadline;
305
- /** Run periodically. Pending secrets expire immediately; historical evidence defaults to 90 days. */
385
+ /**
386
+ * Run periodically. Pending secrets expire immediately; historical evidence defaults to 90
387
+ * days. A retired record is history its holder asked to keep, so it stays for as long as
388
+ * it is not revoked, and once revoked goes like any other.
389
+ */
306
390
  prune(retentionMs?: number): Promise<void>;
307
391
  private invalidateShare;
308
392
  private audit;
309
393
  }
310
394
  /** How many wrong codes a flow takes before it is dead. */
311
395
  export declare const codeAttempts = 5;
396
+ /** How many times a published proof may be refused before its flow is dead. */
397
+ export declare const artifactAttempts = 5;