@ofidj/generator-fidj 3.16.0 → 3.17.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.
|
@@ -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.17.0",
|
|
19
|
+
"@ofidj/node": "^3.17.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {signInErrorMessage, agreementRequired, agreementFromRefusal, verificationPending, pollVerification, rememberSignIn, forgetSignIn, signInHint, type SigninShape} from "@ofidj/entry";
|
|
1
|
+
import {signInErrorMessage, formatDate, optionalPurposes, agreementRequired, agreementFromRefusal, verificationPending, pollVerification, rememberSignIn, forgetSignIn, signInHint, type SigninShape} from "@ofidj/entry";
|
|
2
2
|
import {agreementScreen, bindAgreementScreen, bindPasswordReveal, acceptedAgreement, verificationWait, providerEntry, showEmailEntry, showVersionBadge, escape, masthead, highlightCells, badgeStrip, credentialFields, accountForm, passkeySupported, passkeyAssertion, walletDoor, oidcInteractionMarkup, oidcInteractionStyles, bindOidcInteraction} from "@ofidj/entry/dom";
|
|
3
3
|
import {openProviderWindow, relayProviderAnswer, type ProviderWindow} from "@ofidj/entry/window";
|
|
4
4
|
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
@@ -330,7 +330,10 @@ const PUBLIC_ROUTE = "pub";
|
|
|
330
330
|
// spinner's worth of ceremony, and it says what it is doing.
|
|
331
331
|
let recognising = false;
|
|
332
332
|
function mightBeRecognised() {
|
|
333
|
-
|
|
333
|
+
// Fidj itself, or an app this browser signed in to before: its session lives
|
|
334
|
+
// in the tab, so a new tab would otherwise draw "Continue as …" for somebody
|
|
335
|
+
// Fidj still recognises.
|
|
336
|
+
if (!oidc || !(isFidjItself || signInHint(config.appId))) return false;
|
|
334
337
|
if ((moduleRoute() || "").split("/")[0] === PUBLIC_ROUTE) return false;
|
|
335
338
|
// Not on a screen somebody was sent to by a link.
|
|
336
339
|
//
|
|
@@ -354,7 +357,12 @@ function mightBeRecognised() {
|
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
async function askWhetherFidjKnowsThisBrowser() {
|
|
357
|
-
if (
|
|
360
|
+
if (
|
|
361
|
+
!oidc ||
|
|
362
|
+
!(isFidjItself || signInHint(config.appId)) ||
|
|
363
|
+
sdk.isLoggedIn() ||
|
|
364
|
+
oidc.signedOutHere()
|
|
365
|
+
)
|
|
358
366
|
return false;
|
|
359
367
|
if ((moduleRoute() || "").split("/")[0] === PUBLIC_ROUTE) return false;
|
|
360
368
|
// Not on a screen somebody was sent to by a link.
|
|
@@ -603,7 +611,7 @@ function render() {
|
|
|
603
611
|
// The two ways out, last and together: back to the app, or out of this
|
|
604
612
|
// session. Leaving the app itself is a different decision and stays where
|
|
605
613
|
// the things it erases are listed.
|
|
606
|
-
root.innerHTML = `<section class="content-account">${profileSummary()}<div class="card profile-body">${banner()}${accountForm("account", {linkToken, verificationConfirmed, emailVerified, accountEmail}, {compact: true})}${privacyBlock()}</div></section>`;
|
|
614
|
+
root.innerHTML = `<section class="content-account">${profileSummary()}<div class="card profile-body">${banner()}${emailVerified ? accountRows() : accountForm("account", {linkToken, verificationConfirmed, emailVerified, accountEmail}, {compact: true})}${privacyBlock()}</div></section>`;
|
|
607
615
|
renderNav("account");
|
|
608
616
|
wireAccount("account");
|
|
609
617
|
wireSignOut();
|
|
@@ -913,9 +921,11 @@ function wirePrivacy() {
|
|
|
913
921
|
"click",
|
|
914
922
|
() =>
|
|
915
923
|
void action(async () => {
|
|
924
|
+
// The version published now is the version recorded.
|
|
925
|
+
const agreement = await readAgreement();
|
|
916
926
|
await request(appPath + "/consents", "PUT", {
|
|
917
927
|
terms: true,
|
|
918
|
-
cguVersion:
|
|
928
|
+
...(agreement ? {cguVersion: agreement.version} : {}),
|
|
919
929
|
source: "profile",
|
|
920
930
|
});
|
|
921
931
|
await refresh();
|
|
@@ -984,27 +994,43 @@ function wirePrivacy() {
|
|
|
984
994
|
// about it. It used to be a screen of its own called My privacy, one tab
|
|
985
995
|
// away from the account it was about — so a person looking for their own
|
|
986
996
|
// data had two places to try and no way to tell which.
|
|
997
|
+
// The account, once there is nothing left to verify: one line, and the way to
|
|
998
|
+
// change it, which is on Fidj — the account is Fidj's, not this app's.
|
|
999
|
+
function accountRows() {
|
|
1000
|
+
return `<div class="member-rows"><div class="member-row"><div><strong>Email</strong><small>${escape(accountEmail)} · verified</small></div><a href="${escape(config.dashboardUrl)}/#/my/profile" target="_blank" rel="noopener noreferrer">Manage on Fidj ↗</a></div></div>`;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
987
1003
|
function privacyBlock() {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1004
|
+
const acceptedVersion = String((consent as Record<string, unknown>).termsVersion || "");
|
|
1005
|
+
const agreementHref = `${config.apiEndpoint}/apps/${encodeURIComponent(config.appId)}/agreements/${encodeURIComponent(acceptedVersion)}`;
|
|
1006
|
+
const agreementRow = consent.terms
|
|
1007
|
+
? `<div class="member-row"><div><strong>Service agreement</strong>${acceptedVersion ? `<a class="basis" href="${escape(agreementHref)}" target="_blank" rel="noopener noreferrer">Contract · agreement ${escape(acceptedVersion)} ↗</a>` : '<span class="basis">Contract</span>'}</div><small class="nosw">Part of the service. To stop it, leave the app.</small></div>`
|
|
1008
|
+
: '<div class="member-row"><div><strong>Service agreement</strong><small>Not accepted yet — accept it or leave the app.</small></div><button id="terms" class="primary">Accept</button></div>';
|
|
1009
|
+
const choices = optionalPurposes
|
|
1010
|
+
.map(
|
|
1011
|
+
(purpose) =>
|
|
1012
|
+
`<label class="member-row" for="purpose-${purpose.key}"><div><strong>${escape(purpose.title)}</strong><small>${escape(purpose.description)}</small><span class="basis consent">Consent</span></div><span class="switch"><input type="checkbox" role="switch" id="purpose-${purpose.key}" data-purpose="${purpose.key}" ${consent[purpose.key] ? "checked" : ""}><span class="switch-state" aria-hidden="true">${consent[purpose.key] ? "On" : "Off"}</span></span></label>`,
|
|
1013
|
+
)
|
|
1014
|
+
.join("");
|
|
1015
|
+
const entries = history.length
|
|
1016
|
+
? history
|
|
1017
|
+
.slice()
|
|
1018
|
+
.reverse()
|
|
1019
|
+
.map(
|
|
1020
|
+
(entry) =>
|
|
1021
|
+
`<p>${escape(formatDate(entry.changedAt, "datetime"))} · ${escape(entry.type)} ${entry.granted ? "given" : "withdrawn"}</p>`,
|
|
1022
|
+
)
|
|
1023
|
+
.join("")
|
|
1024
|
+
: "<p>No changes yet.</p>";
|
|
1025
|
+
const leave = roles.includes("Owner")
|
|
1026
|
+
? "<small>You own this app: hand it over or delete it on Fidj before leaving.</small>"
|
|
1027
|
+
: leaving
|
|
1028
|
+
? '<p>Your membership and what this app holds for you will be erased. Your other apps remain available.</p><button id="confirm-leave" class="danger">Leave & erase</button><button id="cancel-leave">Keep my membership</button>'
|
|
1029
|
+
: '<button id="leave" class="danger">Leave & erase</button>';
|
|
1030
|
+
return `<h2>Your membership</h2><div class="member-rows">${agreementRow}${choices}</div>
|
|
1031
|
+
<details class="member-history"><summary>History</summary>${entries}</details>
|
|
1032
|
+
<div class="member-actions"><button id="export">Export</button>${leave}</div>
|
|
1033
|
+
<p class="leaving"><a href="${escape(config.dashboardUrl)}/#/my/gdpr" target="_blank" rel="noopener noreferrer">Open Fidj to manage every app you use ↗</a></p>`;
|
|
1008
1034
|
}
|
|
1009
1035
|
|
|
1010
1036
|
// ------------------------------------------------ signing in, for the provider
|
|
@@ -1031,6 +1057,8 @@ type Interaction = {
|
|
|
1031
1057
|
agreement?: {version: string; text: string} | null;
|
|
1032
1058
|
// Who the consent screen recognises, as the provider names them.
|
|
1033
1059
|
recognisedEmail?: string;
|
|
1060
|
+
// The passkey door's challenge, on the login step.
|
|
1061
|
+
passkey?: {ticket: string; options: unknown};
|
|
1034
1062
|
action: string;
|
|
1035
1063
|
};
|
|
1036
1064
|
let interactionId = "";
|
|
@@ -1155,6 +1183,7 @@ function interactionScreen() {
|
|
|
1155
1183
|
// The agreement is a document read in the browser, like the privacy notice.
|
|
1156
1184
|
agreementHref: details.termsUri || undefined,
|
|
1157
1185
|
recognisedEmail: details.recognisedEmail || undefined,
|
|
1186
|
+
passkey: details.passkey,
|
|
1158
1187
|
logoSrc: "./fidj-logo.png",
|
|
1159
1188
|
})}`;
|
|
1160
1189
|
bindOidcInteraction(root);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {agreementRequired, agreementFromRefusal, signInErrorMessage, rememberSignIn, forgetSignIn, type SigninShape} from "@ofidj/entry";
|
|
1
|
+
import {agreementRequired, agreementFromRefusal, signInErrorMessage, rememberSignIn, forgetSignIn, formatDate, type SigninShape} from "@ofidj/entry";
|
|
2
2
|
import {acceptedAgreement, agreementScreen, bindAgreementScreen, bindPasswordReveal, providerEntry, showEmailEntry, showVersionBadge} from "@ofidj/entry/dom";
|
|
3
3
|
import {openProviderWindow, relayProviderAnswer, type ProviderWindow} from "@ofidj/entry/window";
|
|
4
4
|
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
@@ -223,7 +223,7 @@ function render() {
|
|
|
223
223
|
<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>
|
|
224
224
|
${
|
|
225
225
|
view === "workspace"
|
|
226
|
-
? `<section class="workspace"><div><div class="section-heading"><h2>Your notes</h2><span>${notes.length} saved</span></div>${notes.length ? notes.map((note) => `<article class="card note"><small>${escape(
|
|
226
|
+
? `<section class="workspace"><div><div class="section-heading"><h2>Your notes</h2><span>${notes.length} saved</span></div>${notes.length ? notes.map((note) => `<article class="card note"><small>${escape(formatDate(note.createdAt, "datetime"))}</small><h3>${escape(note.title)}</h3><p>${escape(note.body)}</p></article>`).join("") : `<article class="card empty"><span>✳</span><h3>Room for your next idea.</h3><p>Your saved notes will appear here. Only you can read your notes.</p></article>`}</div><form id="note" class="card"><p class="eyebrow">CAPTURE SOMETHING</p><h2>A fresh note</h2><p>${canWrite ? "You have permission to save notes." : "Ask your app owner for Editor access to save notes."}</p><label for="note-title">Title</label><input id="note-title" maxlength="120" required ${!canWrite ? "disabled" : ""}><label for="note-body">Your note</label><textarea id="note-body" rows="6" maxlength="5000" ${!canWrite ? "disabled" : ""}></textarea><button class="primary" type="submit" ${!canWrite ? "disabled" : ""}>Save note</button><small>Your notes are saved by this app and survive a server restart.</small></form></section>`
|
|
227
227
|
: `<section class="privacy-grid"><article class="card"><p class="eyebrow">ONLY ${escape(settings.title).toUpperCase()}</p><h2>Your choices in this app</h2><p>These preferences are independent of Fidj and your other apps.</p><div class="agreement"><strong>Service agreement</strong><span>${privacy?.consent.terms ? "Accepted" : "Not recorded"}</span>${!privacy?.consent.terms ? '<p>This starter uses a demo agreement. Accept it to record your choice.</p><button id="accept-terms">Accept demo agreement</button>' : "<p>Leaving this app withdraws its required agreement.</p>"}</div>${["analytics", "communications", "optionalData"].map((key, index) => `<label class="toggle"><span><strong>${["Analytics", "Communications", "Optional data"][index]}</strong><small>${["Help improve this app.", "Receive optional news and updates.", "Allow data beyond the essential service."][index]}</small></span><input type="checkbox" data-purpose="${key}" ${privacy?.consent[key] ? "checked" : ""}></label>`).join("")}<button id="export">Export</button></article><article class="card"><h2>Consent history</h2>${
|
|
228
228
|
privacy?.history.length
|
|
229
229
|
? privacy.history
|
|
@@ -231,7 +231,7 @@ function render() {
|
|
|
231
231
|
.reverse()
|
|
232
232
|
.map(
|
|
233
233
|
(entry) =>
|
|
234
|
-
`<p><strong>${escape(entry.type)}</strong> · ${entry.granted ? "Accepted" : "Withdrawn"}<br><small>${escape(
|
|
234
|
+
`<p><strong>${escape(entry.type)}</strong> · ${entry.granted ? "Accepted" : "Withdrawn"}<br><small>${escape(formatDate(entry.changedAt, "datetime"))}</small></p>`,
|
|
235
235
|
)
|
|
236
236
|
.join("")
|
|
237
237
|
: "<p>No changes yet.</p>"
|