@ofidj/generator-fidj 3.14.0 → 3.15.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 CHANGED
@@ -40,7 +40,7 @@ one.
40
40
 
41
41
  > Maintainers: `bin/create-fidj.cjs` is the same scaffolder without Yeoman, for
42
42
  > tooling that runs from a checkout and cannot install `yo` —
43
- > `scripts/local-stack.py` and `fidj-app`'s `create:local` use it.
43
+ > fidj-brain's `scripts/local-stack.py` and `fidj-app`'s `create:local` use it.
44
44
  > `test/parity.test.cjs` fails if either door ever gains an input the other
45
45
  > lacks. It is not an entry point to hand to anyone building an app.
46
46
 
@@ -87,14 +87,14 @@ build when one is supplied, the template's `@ofidj/node` range otherwise — and
87
87
  written to `APP_VERSION` and `app.config.json`. On Fidj's own apps it also shows
88
88
  the version `/v3/status` answers with, so the two can be compared without
89
89
  translation. Keep that range on the version this generator releases: the
90
- workspace's `scripts/check-versions.py` and this package's own suite both refuse
90
+ workspace's `scripts/check-versions.py` (in fidj-brain) and this package's own suite both refuse
91
91
  the mismatch.
92
92
 
93
93
  ## Real validation repository
94
94
 
95
95
  [mleweb](https://github.com/mlefree/mleweb) is the thin command-line validation fixture whose generated website is mlefree.com. Its `package.json` passes the original Mario GIF, welcome text and About/CV/contact links directly into this generator. No custom downstream renderer is required. GitHub CI runs the same generation/build path on Node 22/24 and uploads artifacts. Publishing the site to gh-pages, which is what mlefree.com serves, is a separate deliberate job: it runs on mleweb's `master` or on an explicit workflow dispatch, never from a version branch.
96
96
 
97
- Run `npm test` for scaffolding/CLI safety tests. Generated projects include TypeScript checking and HTTP integration tests for live authorization and privacy isolation. See the [local acceptance walkthrough](../LOCAL-DEVELOPMENT.md). Follow TDD: add/run a failing generator or generated-app test before implementation, make it green, then refactor and rerun regression checks.
97
+ Run `npm test` for scaffolding/CLI safety tests. Generated projects include TypeScript checking and HTTP integration tests for live authorization and privacy isolation. See the [local acceptance walkthrough](../fidj-brain/LOCAL-DEVELOPMENT.md). Follow TDD: add/run a failing generator or generated-app test before implementation, make it green, then refactor and rerun regression checks.
98
98
 
99
99
 
100
100
  ## Releasing to npm
@@ -178,7 +178,7 @@ A compatible issuer, REST API and registered callback without a fragment are pre
178
178
 
179
179
  ## Required service agreement
180
180
 
181
- [The workspace README](../README.md#entry-one-flow-the-same-everywhere) defines
181
+ [The workspace README](../fidj-brain/README.md#entry-one-flow-the-same-everywhere) defines
182
182
  the flow every Fidj sign-in surface follows, and this generator owns the
183
183
  implementation the others render. In short: the first screen asks for an email,
184
184
  a password, **Sign in** and **Create an account**, and gates neither; creating an
@@ -15,8 +15,8 @@
15
15
  "privacy:rehearse": "npm run build && node --test test/server.test.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@ofidj/entry": "^3.14.0",
19
- "@ofidj/node": "^3.14.0"
18
+ "@ofidj/entry": "^3.15.0",
19
+ "@ofidj/node": "^3.15.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^22.0.0",
@@ -1,5 +1,5 @@
1
1
  import {agreementModel, signInErrorMessage, agreementRequired, agreementFromRefusal, verificationPending, pollVerification, rememberSignIn, forgetSignIn, signInHint, type SigninShape} from "@ofidj/entry";
2
- import {agreementScreen, bindAgreementScreen, bindPasswordReveal, acceptedAgreement, verificationWait, providerEntry, showEmailEntry, showVersionBadge, escape, masthead, highlightCells, badgeStrip, credentialFields, accountForm, returnNotice} from "@ofidj/entry/dom";
2
+ import {agreementScreen, bindAgreementScreen, bindPasswordReveal, acceptedAgreement, verificationWait, providerEntry, showEmailEntry, showVersionBadge, escape, masthead, highlightCells, badgeStrip, credentialFields, accountForm, returnNotice, passkeySupported, passkeyAssertion, walletDoor} from "@ofidj/entry/dom";
3
3
  import {openProviderWindow, relayProviderAnswer, type ProviderWindow} from "@ofidj/entry/window";
4
4
  import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
5
5
  import config from "../app.config.json";
@@ -10,6 +10,11 @@ const oidc = config.oidcIssuer ? new FidjOidcClient({issuer: config.oidcIssuer,
10
10
  const root = document.querySelector<HTMLDivElement>("#app")!;
11
11
  showVersionBadge(config.releaseVersion, config.title === "Fidj" ? config.apiEndpoint : undefined);
12
12
  const appPath = `/me/apps/${encodeURIComponent(config.appId)}`;
13
+ // Passkeys (v3 P1-4): one relying party, Fidj's own domain. Only Fidj's own
14
+ // shell runs the ceremony on its page; every other app reaches the passkey
15
+ // through the Fidj window, like the rest of Fidj's sign-in.
16
+ const passkeyHere = config.title === "Fidj" && passkeySupported();
17
+ let signedInWithPasskey = false;
13
18
  let signedIn = false;
14
19
  let emailVerified = false;
15
20
  let anonymous = false;
@@ -620,8 +625,8 @@ function render() {
620
625
  root.innerHTML = `<section class="signin-shell"><div class="signin-intro${config.highlights?.length ? "" : " is-plain"}">${masthead(config.logo, config.title)}
621
626
  <div class="signin-identity"><h1>${escape(config.welcome)}</h1><p class="signin-description">${escape(config.description)}</p></div>
622
627
  ${highlightCells(config.highlights)}</div>
623
- <div class="signin-form"><div>${banner()}<h2>Sign in to ${escape(config.title)}</h2><form id="signin">${credentialFields({email: signInEmail, password: signInPassword})}</form>${config.allowAnonymous ? `<div class="signin-divider"><span>or explore first</span></div><button class="anonymous-entry" id="anonymous">Enter anonymously <span aria-hidden="true">→</span></button><p class="signin-footnote">No account needed to view the content.</p>` : ""}
624
- <div class="signin-trust"><p class="signin-trust-head"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><strong>Your account, with Fidj</strong></p><p>Signing in creates one Fidj account you keep across every app that uses Fidj.</p><p>You choose what this app may store — and can export or erase it at any moment.</p></div></div>
628
+ <div class="signin-form"><div>${banner()}<h2>Sign in to ${escape(config.title)}</h2><form id="signin">${credentialFields({email: signInEmail, password: signInPassword}, {passkey: passkeyHere})}</form>${config.allowAnonymous ? `<div class="signin-divider"><span>or explore first</span></div><button class="anonymous-entry" id="anonymous">Enter anonymously <span aria-hidden="true">→</span></button><p class="signin-footnote">No account needed to view the content.</p>` : ""}
629
+ ${config.title === "Fidj" ? walletDoor() : `<div class="signin-trust"><p class="signin-trust-head"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><strong>Your account, with Fidj</strong></p><p>Signing in creates one Fidj account you keep across every app that uses Fidj.</p><p>You choose what this app may store — and can export or erase it at any moment.</p></div>`}</div>
625
630
  ${badgeStrip(config.badges)}</div></section>`;
626
631
  wireNav();
627
632
  element("anonymous")?.addEventListener("click", () => {
@@ -642,7 +647,7 @@ function render() {
642
647
  config.appId,
643
648
  config.signin === "button"
644
649
  ? ""
645
- : credentialFields({email: signInEmail, password: signInPassword}),
650
+ : credentialFields({email: signInEmail, password: signInPassword}, {passkey: passkeyHere}),
646
651
  isFidjItself,
647
652
  config.signin as SigninShape,
648
653
  );
@@ -718,10 +723,21 @@ function render() {
718
723
  if (!acceptance) return;
719
724
  submitter?.setAttribute("data-busy", "true");
720
725
  void action(async () => {
721
- if (
722
- await refusedBeforeSignIn(signInEmail, signInPassword, false, acceptance)
723
- )
724
- return;
726
+ // A passkey sign-in has no password to send again: the agreement is
727
+ // accepted with a second touch of the same passkey.
728
+ const refused = signedInWithPasskey
729
+ ? await refusedBeforePasskey(acceptance)
730
+ : await refusedBeforeSignIn(signInEmail, signInPassword, false, acceptance);
731
+ if (refused) return;
732
+ await completeSignIn();
733
+ });
734
+ return;
735
+ }
736
+ // The first door (v3): the passkey, with nothing typed.
737
+ if (submitter?.name === "entry" && submitter.value === "passkey") {
738
+ submitter.setAttribute("data-busy", "true");
739
+ void action(async () => {
740
+ if (await refusedBeforePasskey()) return;
725
741
  await completeSignIn();
726
742
  });
727
743
  return;
@@ -764,6 +780,7 @@ async function refusedBeforeSignIn(
764
780
  signup: boolean,
765
781
  acceptance?: {termsAccepted: boolean; termsVersion: string},
766
782
  ): Promise<boolean> {
783
+ signedInWithPasskey = false;
767
784
  try {
768
785
  await sdk.login(email, password, {autoSignup: signup, ...acceptance});
769
786
  pendingAgreement = null;
@@ -788,6 +805,34 @@ async function refusedBeforeSignIn(
788
805
  }
789
806
  }
790
807
 
808
+ // The same answers as a password sign-in, from a passkey: the browser runs the
809
+ // ceremony against Fidj's challenge, and the SDK trades the answer for tokens.
810
+ async function refusedBeforePasskey(
811
+ acceptance?: {termsAccepted: boolean; termsVersion: string},
812
+ ): Promise<boolean> {
813
+ signedInWithPasskey = true;
814
+ try {
815
+ const {options, ticket} = await sdk.passkeyLoginOptions();
816
+ const response = await passkeyAssertion(options);
817
+ await sdk.loginWithPasskey(ticket, response, acceptance);
818
+ pendingAgreement = null;
819
+ awaitingVerification = "";
820
+ return false;
821
+ } catch (error) {
822
+ if (agreementRequired(error)) {
823
+ pendingAgreement = agreementFromRefusal(error) || (await readAgreement());
824
+ if (!pendingAgreement) {
825
+ throw new Error("We cannot reach Fidj right now. Please try again.");
826
+ }
827
+ return true;
828
+ }
829
+ if ((error as any)?.name === "NotAllowedError") {
830
+ throw new Error("No passkey was used. Try again, or sign in with your email.");
831
+ }
832
+ throw new Error(signInErrorMessage(error));
833
+ }
834
+ }
835
+
791
836
  // An API that refused without saying which agreement it wanted. Older ones
792
837
  // cannot say, so the app is asked directly; it is the same agreement unless the
793
838
  // owner published between the two calls.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "3.14.0",
3
+ "version": "3.15.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": {