@ofidj/generator-fidj 3.14.0 → 3.15.1

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
@@ -136,6 +136,13 @@ return them to, hands the answer back when they are done, and closes itself. A
136
136
  browser that will not open one falls back to the full-page redirect. Fidj's own
137
137
  console takes the same door for the same reason — one journey, three apps.
138
138
 
139
+ **The passkey (3.15.0).** Fidj's own shell leads with **Continue with a
140
+ passkey**, the email form under it, and shows the EU wallet door instead of the
141
+ "Your account, with Fidj" note. Every other app keeps its form and reaches the
142
+ passkey through the Fidj window: there is one relying party, Fidj's domain, so a
143
+ passkey cannot be asked for on another app's own page. A passkey sign-in owes the
144
+ same answers as a password one; the agreement is accepted with a second touch.
145
+
139
146
  ## Compose an existing app as a module
140
147
 
141
148
  Build your application for the `/module/` base URL, then pass its public output to the same generator:
@@ -178,7 +185,7 @@ A compatible issuer, REST API and registered callback without a fragment are pre
178
185
 
179
186
  ## Required service agreement
180
187
 
181
- [The workspace README](../README.md#entry-one-flow-the-same-everywhere) defines
188
+ [The workspace README](../fidj-brain/README.md#entry-one-flow-the-same-everywhere) defines
182
189
  the flow every Fidj sign-in surface follows, and this generator owns the
183
190
  implementation the others render. In short: the first screen asks for an email,
184
191
  a password, **Sign in** and **Create an account**, and gates neither; creating an
@@ -24,6 +24,8 @@ try {
24
24
  domain: { type: "string" },
25
25
  module: { type: "string" },
26
26
  "module-entry": { type: "string" },
27
+ "module-version": { type: "string" },
28
+ "module-label": { type: "string" },
27
29
  local: { type: "boolean" },
28
30
  replace: { type: "boolean" },
29
31
  help: { type: "boolean" },
@@ -31,7 +33,7 @@ try {
31
33
  });
32
34
  if (values.help || positionals.length !== 1) {
33
35
  console.log(
34
- "Usage: create-fidj <directory> --app-id <fidjId> [--api-endpoint <url>] [--title <text> --welcome <text> --description <text> --content <html> --domain <hostname>] [--highlight '<heading>|<body>' ...] [--badge <text> ...] [--logo <image>] [--favicon <image>] [--module <built-directory> --module-entry <index.html#/route>] [--oidc-issuer https://api.example/oidc] [--anonymous true|false] [--signin button|inline|both] [--credentials true|false] [--local] [--replace]",
36
+ "Usage: create-fidj <directory> --app-id <fidjId> [--api-endpoint <url>] [--title <text> --welcome <text> --description <text> --content <html> --domain <hostname>] [--highlight '<heading>|<body>' ...] [--badge <text> ...] [--logo <image>] [--favicon <image>] [--module <built-directory> --module-entry <index.html#/route> [--module-version <x.y.z> --module-label <name>]] [--oidc-issuer https://api.example/oidc] [--anonymous true|false] [--signin button|inline|both] [--credentials true|false] [--local] [--replace]",
35
37
  );
36
38
  process.exitCode = values.help ? 0 : 1;
37
39
  } else {
@@ -55,6 +57,8 @@ try {
55
57
  domain: values.domain,
56
58
  module: values.module,
57
59
  moduleEntry: values["module-entry"],
60
+ moduleVersion: values["module-version"],
61
+ moduleLabel: values["module-label"],
58
62
  local: values.local || process.env.FIDJ_LOCAL === "true",
59
63
  replace: values.replace,
60
64
  });
@@ -20,6 +20,8 @@ const TEXT_OPTIONS = [
20
20
  "domain",
21
21
  "module",
22
22
  "module-entry",
23
+ "module-version",
24
+ "module-label",
23
25
  "logo",
24
26
  "favicon",
25
27
  ];
@@ -98,6 +100,8 @@ module.exports = class extends Generator {
98
100
  favicon: this.options.favicon,
99
101
  module: this.options.module,
100
102
  moduleEntry: this.options["module-entry"],
103
+ moduleVersion: this.options["module-version"],
104
+ moduleLabel: this.options["module-label"],
101
105
  domain: this.options.domain,
102
106
  sdkPath: this.options["sdk-path"] || process.env.FIDJ_SDK_DIR,
103
107
  entryPath: this.options["entry-path"] || process.env.FIDJ_ENTRY_DIR,
@@ -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.1",
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";
@@ -8,8 +8,17 @@ import "@ofidj/entry/style.css";
8
8
  const sdk = new FidjNodeService();
9
9
  const oidc = config.oidcIssuer ? new FidjOidcClient({issuer: config.oidcIssuer, clientId: config.appId, redirectUri: window.location.origin + window.location.pathname, apiEndpoint: config.apiEndpoint, storage: sessionStorage}) : null;
10
10
  const root = document.querySelector<HTMLDivElement>("#app")!;
11
- showVersionBadge(config.releaseVersion, config.title === "Fidj" ? config.apiEndpoint : undefined);
11
+ showVersionBadge(
12
+ config.releaseVersion,
13
+ config.title === "Fidj" ? config.apiEndpoint : undefined,
14
+ config.moduleVersion ? {name: config.moduleLabel || "module", version: config.moduleVersion} : undefined,
15
+ );
12
16
  const appPath = `/me/apps/${encodeURIComponent(config.appId)}`;
17
+ // Passkeys (v3 P1-4): one relying party, Fidj's own domain. Only Fidj's own
18
+ // shell runs the ceremony on its page; every other app reaches the passkey
19
+ // through the Fidj window, like the rest of Fidj's sign-in.
20
+ const passkeyHere = config.title === "Fidj" && passkeySupported();
21
+ let signedInWithPasskey = false;
13
22
  let signedIn = false;
14
23
  let emailVerified = false;
15
24
  let anonymous = false;
@@ -620,8 +629,8 @@ function render() {
620
629
  root.innerHTML = `<section class="signin-shell"><div class="signin-intro${config.highlights?.length ? "" : " is-plain"}">${masthead(config.logo, config.title)}
621
630
  <div class="signin-identity"><h1>${escape(config.welcome)}</h1><p class="signin-description">${escape(config.description)}</p></div>
622
631
  ${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>
632
+ <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>` : ""}
633
+ ${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
634
  ${badgeStrip(config.badges)}</div></section>`;
626
635
  wireNav();
627
636
  element("anonymous")?.addEventListener("click", () => {
@@ -642,7 +651,7 @@ function render() {
642
651
  config.appId,
643
652
  config.signin === "button"
644
653
  ? ""
645
- : credentialFields({email: signInEmail, password: signInPassword}),
654
+ : credentialFields({email: signInEmail, password: signInPassword}, {passkey: passkeyHere}),
646
655
  isFidjItself,
647
656
  config.signin as SigninShape,
648
657
  );
@@ -718,10 +727,21 @@ function render() {
718
727
  if (!acceptance) return;
719
728
  submitter?.setAttribute("data-busy", "true");
720
729
  void action(async () => {
721
- if (
722
- await refusedBeforeSignIn(signInEmail, signInPassword, false, acceptance)
723
- )
724
- return;
730
+ // A passkey sign-in has no password to send again: the agreement is
731
+ // accepted with a second touch of the same passkey.
732
+ const refused = signedInWithPasskey
733
+ ? await refusedBeforePasskey(acceptance)
734
+ : await refusedBeforeSignIn(signInEmail, signInPassword, false, acceptance);
735
+ if (refused) return;
736
+ await completeSignIn();
737
+ });
738
+ return;
739
+ }
740
+ // The first door (v3): the passkey, with nothing typed.
741
+ if (submitter?.name === "entry" && submitter.value === "passkey") {
742
+ submitter.setAttribute("data-busy", "true");
743
+ void action(async () => {
744
+ if (await refusedBeforePasskey()) return;
725
745
  await completeSignIn();
726
746
  });
727
747
  return;
@@ -764,6 +784,7 @@ async function refusedBeforeSignIn(
764
784
  signup: boolean,
765
785
  acceptance?: {termsAccepted: boolean; termsVersion: string},
766
786
  ): Promise<boolean> {
787
+ signedInWithPasskey = false;
767
788
  try {
768
789
  await sdk.login(email, password, {autoSignup: signup, ...acceptance});
769
790
  pendingAgreement = null;
@@ -788,6 +809,34 @@ async function refusedBeforeSignIn(
788
809
  }
789
810
  }
790
811
 
812
+ // The same answers as a password sign-in, from a passkey: the browser runs the
813
+ // ceremony against Fidj's challenge, and the SDK trades the answer for tokens.
814
+ async function refusedBeforePasskey(
815
+ acceptance?: {termsAccepted: boolean; termsVersion: string},
816
+ ): Promise<boolean> {
817
+ signedInWithPasskey = true;
818
+ try {
819
+ const {options, ticket} = await sdk.passkeyLoginOptions();
820
+ const response = await passkeyAssertion(options);
821
+ await sdk.loginWithPasskey(ticket, response, acceptance);
822
+ pendingAgreement = null;
823
+ awaitingVerification = "";
824
+ return false;
825
+ } catch (error) {
826
+ if (agreementRequired(error)) {
827
+ pendingAgreement = agreementFromRefusal(error) || (await readAgreement());
828
+ if (!pendingAgreement) {
829
+ throw new Error("We cannot reach Fidj right now. Please try again.");
830
+ }
831
+ return true;
832
+ }
833
+ if ((error as any)?.name === "NotAllowedError") {
834
+ throw new Error("No passkey was used. Try again, or sign in with your email.");
835
+ }
836
+ throw new Error(signInErrorMessage(error));
837
+ }
838
+ }
839
+
791
840
  // An API that refused without saying which agreement it wanted. Older ones
792
841
  // cannot say, so the app is asked directly; it is the same agreement unless the
793
842
  // owner published between the two calls.
package/lib/scaffold.cjs CHANGED
@@ -230,6 +230,10 @@ function scaffold(destination, options) {
230
230
  logo: logo ? "./brand/logo" + logo.extension : "./fidj-logo.png",
231
231
  favicon: favicon ? "./brand/favicon" + favicon.extension : "./fidj-logo.png",
232
232
  moduleEntry: appModule?.entry || "",
233
+ // A mounted module with its own patch (Fidj's console): the badge names
234
+ // it between the SDK and the API.
235
+ moduleVersion: appModule ? options.moduleVersion || "" : "",
236
+ moduleLabel: options.moduleLabel || "module",
233
237
  moduleMount: null,
234
238
  domain: options.domain,
235
239
  };
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.1",
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": {