@ofidj/generator-fidj 1.2.0 → 1.4.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/bin/create-fidj.cjs +3 -1
- package/generators/app/index.js +2 -0
- package/generators/app/templates/typescript/app.config.json +1 -0
- package/generators/app/templates/typescript/src/content.ts +170 -3
- package/generators/app/templates/typescript/src/main.ts +14 -3
- package/generators/app/templates/typescript/src/service-agreement.ts +30 -7
- package/lib/scaffold.cjs +10 -0
- package/package.json +1 -1
package/bin/create-fidj.cjs
CHANGED
|
@@ -18,6 +18,7 @@ try {
|
|
|
18
18
|
logo: { type: "string" },
|
|
19
19
|
favicon: { type: "string" },
|
|
20
20
|
anonymous: { type: "string" },
|
|
21
|
+
credentials: { type: "string" },
|
|
21
22
|
domain: { type: "string" },
|
|
22
23
|
module: { type: "string" },
|
|
23
24
|
"module-entry": { type: "string" },
|
|
@@ -28,7 +29,7 @@ try {
|
|
|
28
29
|
});
|
|
29
30
|
if (values.help || positionals.length !== 1) {
|
|
30
31
|
console.log(
|
|
31
|
-
"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] [--local] [--replace]",
|
|
32
|
+
"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] [--credentials true|false] [--local] [--replace]",
|
|
32
33
|
);
|
|
33
34
|
process.exitCode = values.help ? 0 : 1;
|
|
34
35
|
} else {
|
|
@@ -46,6 +47,7 @@ try {
|
|
|
46
47
|
logo: values.logo,
|
|
47
48
|
favicon: values.favicon,
|
|
48
49
|
anonymous: values.anonymous,
|
|
50
|
+
credentials: values.credentials,
|
|
49
51
|
domain: values.domain,
|
|
50
52
|
module: values.module,
|
|
51
53
|
moduleEntry: values["module-entry"],
|
package/generators/app/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const TEXT_OPTIONS = [
|
|
|
14
14
|
"description",
|
|
15
15
|
"content",
|
|
16
16
|
"anonymous",
|
|
17
|
+
"credentials",
|
|
17
18
|
"domain",
|
|
18
19
|
"module",
|
|
19
20
|
"module-entry",
|
|
@@ -87,6 +88,7 @@ module.exports = class extends Generator {
|
|
|
87
88
|
description: this.options.description,
|
|
88
89
|
content: this.options.content,
|
|
89
90
|
anonymous: this.options.anonymous,
|
|
91
|
+
credentials: this.options.credentials,
|
|
90
92
|
highlights: repeated("highlight", this.options.highlight),
|
|
91
93
|
badges: repeated("badge", this.options.badge),
|
|
92
94
|
logo: this.options.logo,
|
|
@@ -220,6 +220,12 @@ function navigate(route: string) {
|
|
|
220
220
|
if (route !== currentRoute()) window.history.pushState(null, "", "#/" + route);
|
|
221
221
|
if (!busy) render();
|
|
222
222
|
}
|
|
223
|
+
// The credential fields, in one place because the entry now shows them beside
|
|
224
|
+
// the Fidj door rather than instead of it.
|
|
225
|
+
function credentialFields() {
|
|
226
|
+
return `<label for="email">Email</label><input id="email" type="email" value="${escape(signInEmail)}" placeholder="you@company.com" autocomplete="username"><div class="field-head"><label for="password">Password</label><a href="#/forgot">Forgot?</a></div><div class="password-field"><input id="password" type="password" value="${escape(signInPassword)}" placeholder="••••••••••" autocomplete="current-password"><button type="button" id="reveal" aria-controls="password">Show</button></div><button class="primary" type="submit" name="entry" value="credentials">Continue</button><button class="secondary" type="submit" name="signup" value="true">Create an account</button>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
223
229
|
function moduleRoute() {
|
|
224
230
|
const route = window.location.hash.slice(2).split("?")[0];
|
|
225
231
|
if (
|
|
@@ -268,6 +274,20 @@ function startModule() {
|
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
function render() {
|
|
277
|
+
// The provider handed this person here to answer a question. Nothing else
|
|
278
|
+
// this app might want to show belongs on the screen until they have.
|
|
279
|
+
if (interactionId) {
|
|
280
|
+
if (interactionFailed) {
|
|
281
|
+
root.innerHTML = `<section class="card"><p role="alert" class="error">${escape(message)}</p><p><a href="#/signin">Back to sign in</a></p></section>`;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!interaction) {
|
|
285
|
+
root.innerHTML = '<p role="status">Loading…</p>';
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
interactionScreen();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
271
291
|
if (!initialized) {
|
|
272
292
|
root.innerHTML = '<p role="status">Loading your session…</p>';
|
|
273
293
|
return;
|
|
@@ -370,7 +390,11 @@ function render() {
|
|
|
370
390
|
navigate("content");
|
|
371
391
|
});
|
|
372
392
|
if (oidc && element("signin"))
|
|
373
|
-
element("signin")!.innerHTML = providerEntry(
|
|
393
|
+
element("signin")!.innerHTML = providerEntry(
|
|
394
|
+
config.title,
|
|
395
|
+
config.appId,
|
|
396
|
+
config.ownCredentials ? credentialFields() : "",
|
|
397
|
+
);
|
|
374
398
|
element("forget-hint")?.addEventListener("click", () => {
|
|
375
399
|
forgetSignIn(config.appId);
|
|
376
400
|
render();
|
|
@@ -391,9 +415,16 @@ function render() {
|
|
|
391
415
|
render();
|
|
392
416
|
return;
|
|
393
417
|
}
|
|
394
|
-
const
|
|
418
|
+
const submitter = event.submitter as HTMLButtonElement | null;
|
|
419
|
+
const signup = submitter?.name === "signup";
|
|
420
|
+
// Which door was used. The Fidj one leaves for the provider; the credential
|
|
421
|
+
// one signs in here, which is why it is the app's own form and not Fidj's.
|
|
422
|
+
const throughFidj = submitter?.name === "entry" && submitter.value === "fidj";
|
|
395
423
|
void action(async () => {
|
|
396
|
-
if (oidc) {window.location.assign(await oidc.beginLogin()); return;}
|
|
424
|
+
if (oidc && throughFidj) {window.location.assign(await oidc.beginLogin()); return;}
|
|
425
|
+
if (oidc && (!email || !password)) {
|
|
426
|
+
throw new Error("Enter your email and password, or sign in with Fidj.");
|
|
427
|
+
}
|
|
397
428
|
try {
|
|
398
429
|
await sdk.login(email, password, { autoSignup: signup, ...acceptance });
|
|
399
430
|
} catch (error) {
|
|
@@ -502,6 +533,128 @@ function accountForm(route: string) {
|
|
|
502
533
|
: `<h2>My Fidj account</h2><p>Your identity is shared across your apps. Privacy choices remain separate for each app.</p><p id="verification-status">${emailVerified ? "Your email address is verified." : "Your email is not verified yet."}</p><button id="check-verification">Refresh verification status</button>${emailVerified ? "" : '<button id="resend-verification">Send verification email</button>'}<p><a href="#/forgot">Reset my password</a></p><button id="continue-app" class="primary">Continue to ${escape(config.title)}</button>`;
|
|
503
534
|
}
|
|
504
535
|
|
|
536
|
+
|
|
537
|
+
// ------------------------------------------------ signing in, for the provider
|
|
538
|
+
//
|
|
539
|
+
// When Fidj's provider needs a person to identify themselves it hands them to a
|
|
540
|
+
// Fidj front end — this one — rather than serving a page of its own. So this
|
|
541
|
+
// screen belongs to the provider's conversation, not to this app's: it says who
|
|
542
|
+
// is asking, collects what is being asked for, and posts it straight back.
|
|
543
|
+
//
|
|
544
|
+
// The post is a real form navigation, not a fetch: the provider answers with a
|
|
545
|
+
// redirect that carries the person onward through the authorization, and only a
|
|
546
|
+
// navigation can follow it. The single-use token comes from the context call,
|
|
547
|
+
// which is the only thing that reads the interaction cookie.
|
|
548
|
+
type Interaction = {
|
|
549
|
+
prompt: string;
|
|
550
|
+
csrf: string;
|
|
551
|
+
app: { id: string; title: string; description: string };
|
|
552
|
+
scopes: string[];
|
|
553
|
+
termsUri: string;
|
|
554
|
+
privacyUri: string;
|
|
555
|
+
action: string;
|
|
556
|
+
};
|
|
557
|
+
let interactionId = "";
|
|
558
|
+
let interactionError = "";
|
|
559
|
+
let interaction: Interaction | null = null;
|
|
560
|
+
let interactionFailed = false;
|
|
561
|
+
|
|
562
|
+
const scopeMeaning: Record<string, string> = {
|
|
563
|
+
openid: "An identity specific to this app",
|
|
564
|
+
profile: "Your display name",
|
|
565
|
+
email: "Your email and verification status",
|
|
566
|
+
offline_access: "Stay signed in",
|
|
567
|
+
"fidj:api": "Use Fidj account and privacy services for this app",
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const refusals: Record<string, string> = {
|
|
571
|
+
credentials: "We could not sign you in. Check your email and password.",
|
|
572
|
+
signup:
|
|
573
|
+
"Could not create an account. Use a valid email and a password of at least 12 characters, or sign in to your existing account.",
|
|
574
|
+
agreement: "Accept the app's service agreement to continue.",
|
|
575
|
+
refused: "That could not be completed. Please try again.",
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
function readInteraction() {
|
|
579
|
+
const query = window.location.hash.slice(2).split("?")[1] || "";
|
|
580
|
+
const parameters = new URLSearchParams(query);
|
|
581
|
+
const uid = parameters.get("interaction") || "";
|
|
582
|
+
if (!uid) return false;
|
|
583
|
+
interactionId = uid;
|
|
584
|
+
interactionError = parameters.get("error") || "";
|
|
585
|
+
// The address is cleaned immediately: the interaction id is single-use and has
|
|
586
|
+
// no business staying in history or in a shared link.
|
|
587
|
+
window.history.replaceState(null, "", "#/signin");
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function loadInteraction() {
|
|
592
|
+
const endpoint = new URL(
|
|
593
|
+
`/oidc/interaction/${encodeURIComponent(interactionId)}/context`,
|
|
594
|
+
config.apiEndpoint,
|
|
595
|
+
);
|
|
596
|
+
const response = await fetch(endpoint.href, {
|
|
597
|
+
credentials: "include",
|
|
598
|
+
headers: {Accept: "application/json"},
|
|
599
|
+
signal: AbortSignal.timeout(10000),
|
|
600
|
+
});
|
|
601
|
+
if (!response.ok) throw new Error("This sign-in has expired. Start again from the app.");
|
|
602
|
+
interaction = (await response.json()) as Interaction;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function interactionScreen() {
|
|
606
|
+
const details = interaction!;
|
|
607
|
+
const asking = escape(details.app.title);
|
|
608
|
+
const notice = interactionError
|
|
609
|
+
? `<p role="alert" class="error">${escape(refusals[interactionError] || refusals.refused)}</p>`
|
|
610
|
+
: "";
|
|
611
|
+
const action = new URL(details.action, config.apiEndpoint).href;
|
|
612
|
+
const body =
|
|
613
|
+
details.prompt === "login"
|
|
614
|
+
? `<h2>Sign in to continue to ${asking}</h2>
|
|
615
|
+
<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>
|
|
616
|
+
${notice}
|
|
617
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
618
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
619
|
+
<label for="email">Email</label><input id="email" name="email" type="email" autocomplete="username" required>
|
|
620
|
+
<div class="field-head"><label for="password">Password</label><a href="${escape(config.dashboardUrl)}/#/forgot">Forgot?</a></div>
|
|
621
|
+
<div class="password-field"><input id="password" name="password" type="password" autocomplete="current-password" required><button type="button" id="reveal" aria-controls="password">Show</button></div>
|
|
622
|
+
<button class="primary" type="submit" name="action" value="continue">Sign in</button>
|
|
623
|
+
<button class="secondary" type="submit" name="action" value="signup">Create a Fidj account</button>
|
|
624
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
625
|
+
</form>`
|
|
626
|
+
: `<h2>Continue to ${asking}</h2>
|
|
627
|
+
<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>
|
|
628
|
+
${notice}
|
|
629
|
+
<ul class="scope-list">${details.scopes
|
|
630
|
+
.filter((scope) => scopeMeaning[scope])
|
|
631
|
+
.map((scope) => `<li>${escape(scopeMeaning[scope])}</li>`)
|
|
632
|
+
.join("")}</ul>
|
|
633
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
634
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
635
|
+
<label class="agreement-choice"><input type="checkbox" name="terms" value="true" required><span>I accept ${asking}'s service agreement.</span></label>
|
|
636
|
+
${details.termsUri ? `<p class="fineprint"><a href="${escape(details.termsUri)}" target="_blank" rel="noopener noreferrer">Service agreement</a>${details.privacyUri ? ` · <a href="${escape(details.privacyUri)}" target="_blank" rel="noopener noreferrer">Privacy notice</a>` : ""}</p>` : ""}
|
|
637
|
+
<button class="primary" type="submit" name="action" value="continue">Allow and continue</button>
|
|
638
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
639
|
+
</form>`;
|
|
640
|
+
|
|
641
|
+
root.innerHTML = `<section class="signin-shell"><div class="signin-intro is-plain"><header class="signin-masthead"><img class="app-mark" src="${escape(config.logo)}" alt=""><strong>${escape(config.title)}</strong></header>
|
|
642
|
+
<div class="signin-identity"><h1>Your identity.<br>Your choices.</h1><p class="signin-description">One account across every app that uses Fidj, and a separate set of choices for each one.</p></div>
|
|
643
|
+
${highlights()}</div>
|
|
644
|
+
<div class="signin-form"><div>${body}</div>
|
|
645
|
+
<div class="signin-trust"><p class="signin-trust-head"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><strong>What Fidj is</strong></p><p>Fidj holds your account so each app does not have to. You can see every app you use, what it holds, and take it back — at any time.</p></div></div>
|
|
646
|
+
${badges()}</section>`;
|
|
647
|
+
|
|
648
|
+
element("reveal")?.addEventListener("click", () => {
|
|
649
|
+
const field = element<HTMLInputElement>("password");
|
|
650
|
+
const button = element("reveal");
|
|
651
|
+
if (!field || !button) return;
|
|
652
|
+
const hidden = field.type === "password";
|
|
653
|
+
field.type = hidden ? "text" : "password";
|
|
654
|
+
button.textContent = hidden ? "Hide" : "Show";
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
505
658
|
function renderAccount(route: string) {
|
|
506
659
|
root.innerHTML = `<section class="signin-shell"><div class="signin-intro is-plain"><header class="signin-masthead"><img class="app-mark" src="${escape(config.logo)}" alt=""><strong>${escape(config.title)}</strong></header>
|
|
507
660
|
<div class="signin-identity"><h1>Your account.<br>Your control.</h1><p class="signin-description">Secure access to the apps you use, with one Fidj identity.</p></div>
|
|
@@ -569,7 +722,21 @@ function wireAccount(route: string) {
|
|
|
569
722
|
}
|
|
570
723
|
window.addEventListener("hashchange", render);
|
|
571
724
|
render();
|
|
725
|
+
if (readInteraction()) {
|
|
726
|
+
render();
|
|
727
|
+
void loadInteraction()
|
|
728
|
+
.catch((error) => {
|
|
729
|
+
interactionFailed = true;
|
|
730
|
+
failed = true;
|
|
731
|
+
message =
|
|
732
|
+
error instanceof Error
|
|
733
|
+
? error.message
|
|
734
|
+
: "This sign-in could not be loaded. Start again from the app.";
|
|
735
|
+
})
|
|
736
|
+
.finally(render);
|
|
737
|
+
}
|
|
572
738
|
void action(async () => {
|
|
739
|
+
if (interactionId) return;
|
|
573
740
|
if (oidc && new URL(window.location.href).searchParams.has("state")) {
|
|
574
741
|
const callback = new URL(window.location.href);
|
|
575
742
|
window.history.replaceState(null, "", window.location.pathname + "#/content");
|
|
@@ -13,11 +13,16 @@ type Settings = {
|
|
|
13
13
|
localDemo: boolean;
|
|
14
14
|
releaseVersion: string;
|
|
15
15
|
oidcIssuer?: string;
|
|
16
|
+
// An owner may let their own app collect the credential beside the Fidj door.
|
|
17
|
+
// Off unless asked for: the promise the entry makes otherwise is that this app
|
|
18
|
+
// never sees a password.
|
|
19
|
+
ownCredentials?: boolean;
|
|
16
20
|
};
|
|
17
21
|
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
18
22
|
const sdk = new FidjNodeService();
|
|
19
|
-
// When the app is configured with a provider it
|
|
20
|
-
//
|
|
23
|
+
// When the app is configured with a provider it hands the person to Fidj and
|
|
24
|
+
// gets a code back, seeing no password — unless its owner asked for a form of
|
|
25
|
+
// its own beside that door.
|
|
21
26
|
let oidc: FidjOidcClient | null = null;
|
|
22
27
|
let settings: Settings;
|
|
23
28
|
let session: Session | null = null;
|
|
@@ -64,6 +69,12 @@ async function api(path: string, method = "GET", data?: unknown) {
|
|
|
64
69
|
}
|
|
65
70
|
return result;
|
|
66
71
|
}
|
|
72
|
+
// The credential fields, in one place because the entry shows them beside the
|
|
73
|
+
// Fidj door rather than instead of it.
|
|
74
|
+
function credentialFields() {
|
|
75
|
+
return `<label for="email">Email</label><input id="email" type="email" value="${escape(signInEmail)}" autocomplete="username"><label for="password">Password</label><input id="password" type="password" value="${escape(signInPassword)}" autocomplete="current-password"><button class="primary" type="submit" name="entry" value="credentials">Continue</button>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
async function load() {
|
|
68
79
|
session = await api("session");
|
|
69
80
|
// Remembered on this app's own origin so the entry can offer to continue as
|
|
@@ -97,7 +108,7 @@ function render() {
|
|
|
97
108
|
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
98
109
|
${
|
|
99
110
|
!session
|
|
100
|
-
? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2>${oidc ? providerEntry(settings.title, settings.appId) : `<p>Sign in with your Fidj account.</p
|
|
111
|
+
? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2>${oidc ? providerEntry(settings.title, settings.appId, settings.ownCredentials ? credentialFields() : "") : `<p>Sign in with your Fidj account.</p>${credentialFields()}${agreementMarkup()}<button class="primary" type="submit">Continue</button>${settings.localDemo ? `<div class="demo"><strong>Try the local example</strong><p>Alex owns the app. Maya and Sam start with the Free role.</p><button type="button" data-demo="alex">Alex · owner</button><button type="button" data-demo="maya">Maya · member</button><button type="button" data-demo="sam">Sam · member</button></div>` : ""}`}</form></section>`
|
|
101
112
|
: `
|
|
102
113
|
<div class="page-heading"><div><p class="eyebrow">YOUR WORKSPACE</p><h1>A place to think.</h1><p>${escape(session.username)} <span class="roles">${session.roles.map(escape).join(" · ") || "No assigned roles"}</span></p></div><button id="signout">Sign out</button></div>
|
|
103
114
|
<nav><button id="workspace-tab" class="${view === "workspace" ? "selected" : ""}">My notes</button><button id="privacy-tab" class="${view === "privacy" ? "selected" : ""}">My privacy</button><button id="refresh">Refresh access</button></nav>
|
|
@@ -94,7 +94,21 @@ export function forgetSignIn(appId: string) {
|
|
|
94
94
|
} catch {}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
// Both doors, not one. An app that delegates to Fidj still has people who would
|
|
98
|
+
// rather type an address and a password than be sent somewhere, and people Fidj
|
|
99
|
+
// already recognises who should not have to. So the entry offers the credential
|
|
100
|
+
// form and the Fidj entry together, and leads with whichever fits what this
|
|
101
|
+
// browser knows: a remembered address puts Fidj first, no memory puts the form
|
|
102
|
+
// first.
|
|
103
|
+
//
|
|
104
|
+
// The trade-off is stated where it is made: on this path the app's own page
|
|
105
|
+
// handles the Fidj password, so it is the app — not only Fidj — that must be
|
|
106
|
+
// trusted with it. The Fidj entry beside it never is.
|
|
107
|
+
export function providerEntry(
|
|
108
|
+
title: string,
|
|
109
|
+
appId: string,
|
|
110
|
+
credentials: string,
|
|
111
|
+
) {
|
|
98
112
|
const escapeText = (value: unknown) =>
|
|
99
113
|
String(value ?? "").replace(
|
|
100
114
|
/[&<>"']/g,
|
|
@@ -104,11 +118,20 @@ export function providerEntry(title: string, appId: string) {
|
|
|
104
118
|
]!,
|
|
105
119
|
);
|
|
106
120
|
const hint = signInHint(appId);
|
|
121
|
+
const both = Boolean(credentials);
|
|
107
122
|
const lead = hint
|
|
108
|
-
? `<p class="signin-lead">You signed in here with Fidj before. ${escapeText(title)} accounts are Fidj accounts
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
? `<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
|
+
: both
|
|
125
|
+
? `<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
|
+
const fidj = hint
|
|
128
|
+
? `<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>`;
|
|
130
|
+
if (!both) return lead + agreementMarkup() + fidj;
|
|
131
|
+
// Both doors. A remembered address puts Fidj first because it is one tap; no
|
|
132
|
+
// memory puts the form first, because that is what the person came to do.
|
|
133
|
+
const divider = `<div class="signin-divider"><span>or</span></div>`;
|
|
134
|
+
return hint
|
|
135
|
+
? lead + agreementMarkup() + fidj + divider + credentials
|
|
136
|
+
: lead + agreementMarkup() + credentials + divider + fidj;
|
|
114
137
|
}
|
package/lib/scaffold.cjs
CHANGED
|
@@ -25,6 +25,15 @@ function scaffold(destination, options) {
|
|
|
25
25
|
const anonymous = options.anonymous ?? true;
|
|
26
26
|
if (![true, false, "true", "false"].includes(anonymous))
|
|
27
27
|
throw new Error("--anonymous must be true or false.");
|
|
28
|
+
// An app that delegates to the provider collects no password: that is the
|
|
29
|
+
// promise its entry makes, and it holds only while the app never sees one.
|
|
30
|
+
// An owner can decide otherwise for their own app — some people would rather
|
|
31
|
+
// type an address than be sent somewhere — and then it is the app, not only
|
|
32
|
+
// Fidj, that must be trusted with the credential. Off by default, so the
|
|
33
|
+
// promise stays the default.
|
|
34
|
+
const credentials = options.credentials ?? false;
|
|
35
|
+
if (![true, false, "true", "false"].includes(credentials))
|
|
36
|
+
throw new Error("--credentials must be true or false.");
|
|
28
37
|
const name = options.name || path.basename(path.resolve(destination));
|
|
29
38
|
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name))
|
|
30
39
|
throw new Error(
|
|
@@ -162,6 +171,7 @@ function scaffold(destination, options) {
|
|
|
162
171
|
releaseVersion,
|
|
163
172
|
localDemo: Boolean(options.local),
|
|
164
173
|
allowAnonymous: anonymous === true || anonymous === "true",
|
|
174
|
+
ownCredentials: credentials === true || credentials === "true",
|
|
165
175
|
welcome: options.welcome || title,
|
|
166
176
|
description:
|
|
167
177
|
options.description ||
|