@ofidj/generator-fidj 1.2.0 → 1.3.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.
@@ -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"],
@@ -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,
@@ -5,6 +5,7 @@
5
5
  "title": "My app",
6
6
  "localDemo": false,
7
7
  "allowAnonymous": true,
8
+ "ownCredentials": false,
8
9
  "moduleEntry": "",
9
10
  "moduleMount": null,
10
11
  "description": "A space to explore, with an account that puts you in control.",
@@ -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 (
@@ -370,7 +376,11 @@ function render() {
370
376
  navigate("content");
371
377
  });
372
378
  if (oidc && element("signin"))
373
- element("signin")!.innerHTML = providerEntry(config.title, config.appId);
379
+ element("signin")!.innerHTML = providerEntry(
380
+ config.title,
381
+ config.appId,
382
+ config.ownCredentials ? credentialFields() : "",
383
+ );
374
384
  element("forget-hint")?.addEventListener("click", () => {
375
385
  forgetSignIn(config.appId);
376
386
  render();
@@ -391,9 +401,16 @@ function render() {
391
401
  render();
392
402
  return;
393
403
  }
394
- const signup = (event.submitter as HTMLButtonElement)?.name === "signup";
404
+ const submitter = event.submitter as HTMLButtonElement | null;
405
+ const signup = submitter?.name === "signup";
406
+ // Which door was used. The Fidj one leaves for the provider; the credential
407
+ // one signs in here, which is why it is the app's own form and not Fidj's.
408
+ const throughFidj = submitter?.name === "entry" && submitter.value === "fidj";
395
409
  void action(async () => {
396
- if (oidc) {window.location.assign(await oidc.beginLogin()); return;}
410
+ if (oidc && throughFidj) {window.location.assign(await oidc.beginLogin()); return;}
411
+ if (oidc && (!email || !password)) {
412
+ throw new Error("Enter your email and password, or sign in with Fidj.");
413
+ }
397
414
  try {
398
415
  await sdk.login(email, password, { autoSignup: signup, ...acceptance });
399
416
  } catch (error) {
@@ -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 never sees a password: the
20
- // entry hands the person to Fidj and gets a code back.
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><label for="email">Email</label><input id="email" type="email" value="${escape(signInEmail)}" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" value="${escape(signInPassword)}" autocomplete="current-password" required>${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>`
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
- export function providerEntry(title: string, appId: string) {
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, and this site never sees your password you can also create or use another one.</p>`
109
- : `<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>`;
110
- const action = hint
111
- ? `<button class="primary" type="submit">Continue as ${escapeText(hint)}</button><button type="button" id="forget-hint" class="quiet">Use a different account</button>`
112
- : `<button class="primary" type="submit">Sign in with Fidj</button>`;
113
- return lead + agreementMarkup() + action;
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 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {