@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.
|
|
19
|
-
"@ofidj/node": "^3.
|
|
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
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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.
|