@ofidj/generator-fidj 1.4.2 → 1.6.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.
@@ -407,7 +407,35 @@ function render() {
407
407
  config.title,
408
408
  config.appId,
409
409
  config.ownCredentials ? credentialFields() : "",
410
+ isFidjItself,
410
411
  );
412
+ // On Fidj itself there is nobody to hand the person to, so the credential
413
+ // screen is fetched straight away: what they see is the form, or their name.
414
+ // Once per document, and never once the provider has already answered — a
415
+ // refusal or a cancellation must not bounce them round again.
416
+ if (
417
+ oidc &&
418
+ isFidjItself &&
419
+ // Only from the sign-in screen. Without this it fired on every render where
420
+ // nobody was signed in — including the recovery screens, so a person opening
421
+ // a password-reset link was handed a sign-in form instead of the reset they
422
+ // had asked for, and could never finish.
423
+ element("signin") &&
424
+ !askedProvider &&
425
+ !interactionId &&
426
+ !signedIn &&
427
+ // Nor when the shell has something to say. A password reset ends on this
428
+ // screen with "your password has been changed"; leaving for the provider
429
+ // would swallow the one confirmation the person was waiting for.
430
+ !message
431
+ ) {
432
+ askedProvider = true;
433
+ void (async () => {
434
+ if (!(await providerRendersHere())) return;
435
+ window.location.assign(await oidc.beginLogin());
436
+ })();
437
+ }
438
+
411
439
  element("forget-hint")?.addEventListener("click", () => {
412
440
  forgetSignIn(config.appId);
413
441
  render();
@@ -588,6 +616,36 @@ const refusals: Record<string, string> = {
588
616
  refused: "That could not be completed. Please try again.",
589
617
  };
590
618
 
619
+ // Fidj's own front end, told apart by the one fact it already carries: the
620
+ // dashboard it points people to is itself. "Sign in with Fidj" is the right
621
+ // label on an app that is not Fidj; here it names a provider the person is
622
+ // standing in, and hides the form behind a click that only fetches it.
623
+ let askedProvider = false;
624
+ // Whether the provider renders its sign-in on this front end. A deployment
625
+ // decides that, so the shell asks rather than assumes: hopping to a provider
626
+ // that answers with its own page would land the person on the API's origin,
627
+ // which is the thing this is meant to avoid.
628
+ let signinOnThisUi: boolean | null = null;
629
+ async function providerRendersHere() {
630
+ if (signinOnThisUi !== null) return signinOnThisUi;
631
+ try {
632
+ const response = await fetch(
633
+ new URL("status", config.apiEndpoint.replace(/\/?$/, "/")).href,
634
+ {signal: AbortSignal.timeout(5000)},
635
+ );
636
+ signinOnThisUi = response.ok && (await response.json()).signin === "fidj-ui";
637
+ } catch {
638
+ signinOnThisUi = false;
639
+ }
640
+ return signinOnThisUi;
641
+ }
642
+ const isFidjItself = (() => {
643
+ try {
644
+ return new URL(config.dashboardUrl).origin === window.location.origin;
645
+ } catch {
646
+ return false;
647
+ }
648
+ })();
591
649
  function addressedInteraction() {
592
650
  const query = window.location.hash.slice(2).split("?")[1] || "";
593
651
  return new URLSearchParams(query).get("interaction") || "";
@@ -636,10 +694,14 @@ function interactionScreen() {
636
694
  try {
637
695
  typed = sessionStorage.getItem("fidj.interaction.email") || "";
638
696
  } catch {}
697
+ // Fidj signing into Fidj: saying "the account behind fidj" and "fidj never
698
+ // sees your password" about itself is nonsense in the same family as offering
699
+ // to sign in with Fidj on Fidj.
700
+ const itself = details.app.id === config.appId;
639
701
  const body =
640
702
  details.prompt === "login"
641
- ? `<h2>Sign in to continue to ${asking}</h2>
642
- <p class="signin-lead">This is Fidj, the account behind ${asking}. One account, and separate choices for every app that uses it — ${asking} never sees your password.</p>
703
+ ? `<h2>${itself ? "Sign in to Fidj" : "Sign in to continue to " + asking}</h2>
704
+ <p class="signin-lead">${itself ? "One account across every app that uses Fidj, and a separate set of choices for each one." : `This is Fidj, the account behind ${asking}. One account, and separate choices for every app that uses it — ${asking} never sees your password.`}</p>
643
705
  ${notice}
644
706
  <form method="post" action="${escape(action)}" id="interaction">
645
707
  <input type="hidden" name="csrf" value="${escape(details.csrf)}">
@@ -650,8 +712,8 @@ function interactionScreen() {
650
712
  <button class="secondary" type="submit" name="action" value="signup">Create a Fidj account</button>
651
713
  <button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
652
714
  </form>`
653
- : `<h2>Continue to ${asking}</h2>
654
- <p class="signin-lead">${asking} is asking for the information below. Optional privacy choices stay separate, and you can change them in Fidj at any time.</p>
715
+ : `<h2>${itself ? "Continue to Fidj" : "Continue to " + asking}</h2>
716
+ <p class="signin-lead">${itself ? "Fidj is asking for the information below. Optional privacy choices stay separate for every app, including this one." : `${asking} is asking for the information below. Optional privacy choices stay separate, and you can change them in Fidj at any time.`}</p>
655
717
  ${notice}
656
718
  <ul class="scope-list">${details.scopes
657
719
  .filter((scope) => scopeMeaning[scope])
@@ -108,6 +108,7 @@ export function providerEntry(
108
108
  title: string,
109
109
  appId: string,
110
110
  credentials: string,
111
+ isFidjItself = false,
111
112
  ) {
112
113
  const escapeText = (value: unknown) =>
113
114
  String(value ?? "").replace(
@@ -123,10 +124,12 @@ export function providerEntry(
123
124
  ? `<p class="signin-lead">You signed in here with Fidj before. ${escapeText(title)} accounts are Fidj accounts — continue as yourself, or use another.</p>`
124
125
  : both
125
126
  ? `<p class="signin-lead">${escapeText(title)} accounts are Fidj accounts. Sign in below, or let Fidj do it on its own page — where this site never sees your password.</p>`
126
- : `<p class="signin-lead">${escapeText(title)} accounts are Fidj accounts. You will sign in — or create yours — on Fidj's own page, so this site never sees your password.</p>`;
127
+ : isFidjItself
128
+ ? `<p class="signin-lead">One account across every app that uses Fidj, and a separate set of choices for each one. Sign in, or create yours, on the next screen.</p>`
129
+ : `<p class="signin-lead">${escapeText(title)} accounts are Fidj accounts. You will sign in — or create yours — on Fidj's own page, so this site never sees your password.</p>`;
127
130
  const fidj = hint
128
131
  ? `<button class="primary" type="submit" name="entry" value="fidj">Continue as ${escapeText(hint)}</button><button type="button" id="forget-hint" class="quiet">Use a different account</button>`
129
- : `<button class="${both ? "secondary" : "primary"}" type="submit" name="entry" value="fidj">Sign in with Fidj</button>`;
132
+ : `<button class="${both ? "secondary" : "primary"}" type="submit" name="entry" value="fidj">${isFidjItself ? "Sign in" : "Sign in with Fidj"}</button>`;
130
133
  if (!both) return lead + agreementMarkup() + fidj;
131
134
  // Both doors. A remembered address puts Fidj first because it is one tap; no
132
135
  // memory puts the form first, because that is what the person came to do.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "Generate a TypeScript app with Fidj sign-in, live server-side roles and per-app privacy.",
5
5
  "homepage": "https://fidj.ovh",
6
6
  "bugs": {