@ofidj/generator-fidj 1.1.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.
- 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 +57 -8
- package/generators/app/templates/typescript/src/main.ts +19 -4
- package/generators/app/templates/typescript/src/service-agreement.ts +75 -0
- package/generators/app/templates/typescript/src/style.css +22 -0
- 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,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
|
|
1
|
+
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage, providerEntry, rememberSignIn, forgetSignIn} from "./service-agreement";
|
|
2
2
|
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
3
3
|
import config from "../app.config.json";
|
|
4
4
|
import "./style.css";
|
|
@@ -58,10 +58,20 @@ function badges() {
|
|
|
58
58
|
.map((entry) => `<span>${escape(entry)}</span>`)
|
|
59
59
|
.join("")}</footer>`;
|
|
60
60
|
}
|
|
61
|
+
// Signing out of this app revokes this app's access and nothing else: the Fidj
|
|
62
|
+
// session survives, which is what makes the next app free to enter. That is a
|
|
63
|
+
// good design and a bad surprise, so the notice says what actually happened and
|
|
64
|
+
// where the rest of it lives — on a shared computer the difference is the whole
|
|
65
|
+
// point.
|
|
66
|
+
let leftTheApp = false;
|
|
61
67
|
function banner() {
|
|
62
|
-
return
|
|
63
|
-
|
|
68
|
+
if (!message) return "";
|
|
69
|
+
const role = failed ? "alert" : "status";
|
|
70
|
+
const kind = failed ? "error" : "notice";
|
|
71
|
+
const finish = leftTheApp
|
|
72
|
+
? ` <a href="${escape(config.dashboardUrl)}/#/my/profile" target="_blank" rel="noopener">Sign out of Fidj too</a>`
|
|
64
73
|
: "";
|
|
74
|
+
return `<p role="${role}" class="${kind}">${escape(message)}${finish}</p>`;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
// One navigation bar for every in-app screen, so signing out stays one click
|
|
@@ -83,9 +93,15 @@ function wireNav() {
|
|
|
83
93
|
"click",
|
|
84
94
|
() =>
|
|
85
95
|
void action(async () => {
|
|
96
|
+
const wasSignedIn = signedIn;
|
|
86
97
|
if (signedIn) await sdk.logout(true);
|
|
98
|
+
forgetSignIn(config.appId);
|
|
87
99
|
signedIn = false;
|
|
88
100
|
anonymous = false;
|
|
101
|
+
if (wasSignedIn) {
|
|
102
|
+
leftTheApp = true;
|
|
103
|
+
message = `Signed out of ${config.title}. You are still signed in to Fidj.`;
|
|
104
|
+
}
|
|
89
105
|
navigate("signin");
|
|
90
106
|
}),
|
|
91
107
|
);
|
|
@@ -131,8 +147,13 @@ async function refresh() {
|
|
|
131
147
|
request(appPath + "/consents"),
|
|
132
148
|
request(appPath + "/consents/history").then((result) => result.history),
|
|
133
149
|
]);
|
|
134
|
-
|
|
150
|
+
const me = (await request("/me")).user;
|
|
151
|
+
emailVerified = me?.verified === true;
|
|
135
152
|
signedIn = true;
|
|
153
|
+
// The ID token of a code flow carries only the subject, by design, so the
|
|
154
|
+
// address the entry can offer next time comes from the membership the app
|
|
155
|
+
// just read — not from a claim it does not have.
|
|
156
|
+
rememberSignIn(config.appId, String(me?.poc?.email || me?.username || ""));
|
|
136
157
|
}
|
|
137
158
|
async function action(task: () => Promise<void>) {
|
|
138
159
|
if (busy) return;
|
|
@@ -146,7 +167,10 @@ async function action(task: () => Promise<void>) {
|
|
|
146
167
|
const submit = root.querySelector<HTMLButtonElement>("button.primary");
|
|
147
168
|
if (submit) submit.textContent = "Please wait…";
|
|
148
169
|
failed = false;
|
|
149
|
-
if (initialized)
|
|
170
|
+
if (initialized) {
|
|
171
|
+
message = "";
|
|
172
|
+
leftTheApp = false;
|
|
173
|
+
}
|
|
150
174
|
try {
|
|
151
175
|
await task();
|
|
152
176
|
} catch (error) {
|
|
@@ -196,6 +220,12 @@ function navigate(route: string) {
|
|
|
196
220
|
if (route !== currentRoute()) window.history.pushState(null, "", "#/" + route);
|
|
197
221
|
if (!busy) render();
|
|
198
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
|
+
|
|
199
229
|
function moduleRoute() {
|
|
200
230
|
const route = window.location.hash.slice(2).split("?")[0];
|
|
201
231
|
if (
|
|
@@ -345,7 +375,16 @@ function render() {
|
|
|
345
375
|
anonymous = true;
|
|
346
376
|
navigate("content");
|
|
347
377
|
});
|
|
348
|
-
if (oidc && element("signin"))
|
|
378
|
+
if (oidc && element("signin"))
|
|
379
|
+
element("signin")!.innerHTML = providerEntry(
|
|
380
|
+
config.title,
|
|
381
|
+
config.appId,
|
|
382
|
+
config.ownCredentials ? credentialFields() : "",
|
|
383
|
+
);
|
|
384
|
+
element("forget-hint")?.addEventListener("click", () => {
|
|
385
|
+
forgetSignIn(config.appId);
|
|
386
|
+
render();
|
|
387
|
+
});
|
|
349
388
|
void bindAgreement(element<HTMLFormElement>("signin"), config.title, config.apiEndpoint, config.appId, signInAgreementAccepted);
|
|
350
389
|
element<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
351
390
|
event.preventDefault();
|
|
@@ -362,9 +401,16 @@ function render() {
|
|
|
362
401
|
render();
|
|
363
402
|
return;
|
|
364
403
|
}
|
|
365
|
-
const
|
|
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";
|
|
366
409
|
void action(async () => {
|
|
367
|
-
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
|
+
}
|
|
368
414
|
try {
|
|
369
415
|
await sdk.login(email, password, { autoSignup: signup, ...acceptance });
|
|
370
416
|
} catch (error) {
|
|
@@ -384,8 +430,11 @@ function render() {
|
|
|
384
430
|
() =>
|
|
385
431
|
void action(async () => {
|
|
386
432
|
await sdk.logout(true);
|
|
433
|
+
forgetSignIn(config.appId);
|
|
387
434
|
signedIn = false;
|
|
388
435
|
anonymous = false;
|
|
436
|
+
leftTheApp = true;
|
|
437
|
+
message = `Signed out of ${config.title}. You are still signed in to Fidj.`;
|
|
389
438
|
navigate("signin");
|
|
390
439
|
}),
|
|
391
440
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
|
|
1
|
+
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage, providerEntry, rememberSignIn, forgetSignIn} from "./service-agreement";
|
|
2
2
|
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
3
3
|
import "./style.css";
|
|
4
4
|
import { showVersionBadge } from "./version";
|
|
@@ -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,8 +69,17 @@ 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");
|
|
80
|
+
// Remembered on this app's own origin so the entry can offer to continue as
|
|
81
|
+
// them next time; signing out forgets it.
|
|
82
|
+
if (session?.username) rememberSignIn(settings.appId, session.username);
|
|
69
83
|
[notes, privacy] = await Promise.all([
|
|
70
84
|
api("notes").then((result) => result.notes),
|
|
71
85
|
api("privacy"),
|
|
@@ -94,7 +108,7 @@ function render() {
|
|
|
94
108
|
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
95
109
|
${
|
|
96
110
|
!session
|
|
97
|
-
? `<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 ?
|
|
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>`
|
|
98
112
|
: `
|
|
99
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>
|
|
100
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>
|
|
@@ -157,6 +171,7 @@ function render() {
|
|
|
157
171
|
() =>
|
|
158
172
|
void action(async () => {
|
|
159
173
|
await sdk.logout(true);
|
|
174
|
+
forgetSignIn(settings.appId);
|
|
160
175
|
session = null;
|
|
161
176
|
notes = [];
|
|
162
177
|
privacy = null;
|
|
@@ -60,3 +60,78 @@ export async function bindAgreement(form: HTMLFormElement | null, title: string,
|
|
|
60
60
|
retry.addEventListener("click", load);
|
|
61
61
|
await load();
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
// The entry every app that delegates to the provider renders, in one place
|
|
65
|
+
// because both app shapes render it and they had drifted apart.
|
|
66
|
+
//
|
|
67
|
+
// "Continue with Fidj" alone borrows the grammar of an optional social login —
|
|
68
|
+
// that button always sits next to an email and a password — so on an app whose
|
|
69
|
+
// accounts *are* Fidj accounts, the missing form reads as something broken. The
|
|
70
|
+
// explanation therefore comes before the button, not as reassurance after it,
|
|
71
|
+
// and nothing presupposes an account the person may not have yet.
|
|
72
|
+
const hintKey = (appId: string) => "fidj.entry." + appId;
|
|
73
|
+
|
|
74
|
+
export function signInHint(appId: string) {
|
|
75
|
+
try {
|
|
76
|
+
return localStorage.getItem(hintKey(appId)) || "";
|
|
77
|
+
} catch {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Remembered on this app's own origin, about this app's own member: no
|
|
83
|
+
// cross-site question is asked, and none is answered. Signing out forgets, so a
|
|
84
|
+
// shared browser does not show the next person an address.
|
|
85
|
+
export function rememberSignIn(appId: string, label: string) {
|
|
86
|
+
try {
|
|
87
|
+
if (label) localStorage.setItem(hintKey(appId), label);
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function forgetSignIn(appId: string) {
|
|
92
|
+
try {
|
|
93
|
+
localStorage.removeItem(hintKey(appId));
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
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
|
+
) {
|
|
112
|
+
const escapeText = (value: unknown) =>
|
|
113
|
+
String(value ?? "").replace(
|
|
114
|
+
/[&<>"']/g,
|
|
115
|
+
(char) =>
|
|
116
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
117
|
+
char
|
|
118
|
+
]!,
|
|
119
|
+
);
|
|
120
|
+
const hint = signInHint(appId);
|
|
121
|
+
const both = Boolean(credentials);
|
|
122
|
+
const lead = hint
|
|
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;
|
|
137
|
+
}
|
|
@@ -935,3 +935,25 @@ summary:focus-visible {
|
|
|
935
935
|
.signin-agreement button { margin-block: 0.5rem; padding: 0; border: 0; background: transparent; text-decoration: underline; }
|
|
936
936
|
#agreement-dialog { color: var(--fidj-text); background: var(--fidj-paper); border: 1px solid var(--fidj-line); border-radius: var(--fidj-radius); padding: 1.5rem; max-width: min(40rem, calc(100vw - 2rem)); max-height: 80vh; overflow: auto; }
|
|
937
937
|
#agreement-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
938
|
+
|
|
939
|
+
/* The entry explains itself before it offers its button. A person whose account
|
|
940
|
+
* lives somewhere else needs to know that before they look for the form that is
|
|
941
|
+
* not coming, so this is body text above the action, not fineprint below it. */
|
|
942
|
+
.signin-lead {
|
|
943
|
+
margin: 0 0 18px;
|
|
944
|
+
color: var(--fidj-ink-soft, inherit);
|
|
945
|
+
line-height: 1.5;
|
|
946
|
+
}
|
|
947
|
+
/* Present but not competing: "use a different account" is the way out of a
|
|
948
|
+
* remembered address, not an alternative worth the weight of the primary. */
|
|
949
|
+
button.quiet {
|
|
950
|
+
margin-top: 10px;
|
|
951
|
+
background: none;
|
|
952
|
+
border: none;
|
|
953
|
+
color: inherit;
|
|
954
|
+
text-decoration: underline;
|
|
955
|
+
padding: 6px 0;
|
|
956
|
+
font: inherit;
|
|
957
|
+
cursor: pointer;
|
|
958
|
+
width: auto;
|
|
959
|
+
}
|
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 ||
|