@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
|
package/bin/create-fidj.cjs
CHANGED
|
@@ -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
|
});
|
package/generators/app/index.js
CHANGED
|
@@ -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.
|
|
19
|
-
"@ofidj/node": "^3.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
};
|