@ofidj/generator-fidj 3.17.0 → 3.18.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.17.0",
19
- "@ofidj/node": "^3.17.0"
18
+ "@ofidj/entry": "^3.18.0",
19
+ "@ofidj/node": "^3.18.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^22.0.0",
@@ -1,5 +1,5 @@
1
- import {signInErrorMessage, formatDate, optionalPurposes, 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, passkeySupported, passkeyAssertion, walletDoor, oidcInteractionMarkup, oidcInteractionStyles, bindOidcInteraction} from "@ofidj/entry/dom";
1
+ import {signInErrorMessage, formatDate, 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, passkeySupported, passkeyAssertion, walletDoor, memberCard, 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";
5
5
  import config from "../app.config.json";
@@ -412,16 +412,16 @@ function signInThroughProvider(
412
412
  void action(async () => {
413
413
  let url: string;
414
414
  try {
415
- // Being recognised again is the one thing somebody who has just signed
416
- // out did not ask for, and skipping this is what let a sign-out be undone
417
- // by pressing the door again: the provider still knew the browser and
418
- // answered with a code, no screen at all. Ending the provider session is
419
- // what should make that impossible, and that call can be refused — so the
420
- // door asks rather than assumes, until somebody signs in again.
415
+ // Somebody who has just signed out of this app did not ask to be walked
416
+ // straight back in: the provider still knows the browser and would answer
417
+ // with a code and no screen. So the door asks which account — the consent
418
+ // screen names the one Fidj recognises and offers another. It does not ask
419
+ // for a password: prompt=login told Fidj "this is not me" and ended the
420
+ // Fidj session along with the app's.
421
421
  url = await oidc.beginLogin(
422
422
  options.prompt || options.silent || !oidc.signedOutHere()
423
423
  ? options
424
- : {...options, prompt: "login"},
424
+ : {...options, prompt: "consent"},
425
425
  );
426
426
  } catch (error) {
427
427
  providerWindow?.giveUp();
@@ -917,7 +917,7 @@ function wirePrivacy() {
917
917
  navigate("signin");
918
918
  }),
919
919
  );
920
- element("terms")?.addEventListener(
920
+ element("accept-terms")?.addEventListener(
921
921
  "click",
922
922
  () =>
923
923
  void action(async () => {
@@ -1001,36 +1001,15 @@ function accountRows() {
1001
1001
  }
1002
1002
 
1003
1003
  function privacyBlock() {
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 &amp; erase</button><button id="cancel-leave">Keep my membership</button>'
1029
- : '<button id="leave" class="danger">Leave &amp; 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>`;
1004
+ const version = String((consent as Record<string, unknown>).termsVersion || "");
1005
+ return memberCard({
1006
+ consent,
1007
+ history,
1008
+ agreementHref: `${config.apiEndpoint}/apps/${encodeURIComponent(config.appId)}/agreements/${encodeURIComponent(version)}`,
1009
+ owner: roles.includes("Owner"),
1010
+ leaving,
1011
+ manageHref: `${config.dashboardUrl}/#/my/gdpr`,
1012
+ });
1034
1013
  }
1035
1014
 
1036
1015
  // ------------------------------------------------ signing in, for the provider
@@ -1059,6 +1038,8 @@ type Interaction = {
1059
1038
  recognisedEmail?: string;
1060
1039
  // The passkey door's challenge, on the login step.
1061
1040
  passkey?: {ticket: string; options: unknown};
1041
+ // The recognised person's agreement is on file at the version in force.
1042
+ agreementAccepted?: boolean;
1062
1043
  action: string;
1063
1044
  };
1064
1045
  let interactionId = "";
@@ -1184,6 +1165,7 @@ function interactionScreen() {
1184
1165
  agreementHref: details.termsUri || undefined,
1185
1166
  recognisedEmail: details.recognisedEmail || undefined,
1186
1167
  passkey: details.passkey,
1168
+ agreementAccepted: details.agreementAccepted,
1187
1169
  logoSrc: "./fidj-logo.png",
1188
1170
  })}`;
1189
1171
  bindOidcInteraction(root);
@@ -1,5 +1,5 @@
1
1
  import {agreementRequired, agreementFromRefusal, signInErrorMessage, rememberSignIn, forgetSignIn, formatDate, type SigninShape} from "@ofidj/entry";
2
- import {acceptedAgreement, agreementScreen, bindAgreementScreen, bindPasswordReveal, providerEntry, showEmailEntry, showVersionBadge} from "@ofidj/entry/dom";
2
+ import {acceptedAgreement, agreementScreen, memberCard, 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";
5
5
  import "@ofidj/entry/style.css";
@@ -224,18 +224,18 @@ function render() {
224
224
  ${
225
225
  view === "workspace"
226
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
- : `<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
- privacy?.history.length
229
- ? privacy.history
230
- .slice()
231
- .reverse()
232
- .map(
233
- (entry) =>
234
- `<p><strong>${escape(entry.type)}</strong> · ${entry.granted ? "Accepted" : "Withdrawn"}<br><small>${escape(formatDate(entry.changedAt, "datetime"))}</small></p>`,
235
- )
236
- .join("")
237
- : "<p>No changes yet.</p>"
238
- }<hr><h2>Leave this app</h2><p>This removes your app membership and this starter’s notes. Your Fidj account and other memberships remain.</p>${session.roles.includes("Owner") ? "<p>As the app owner, resolve ownership before leaving.</p>" : leaving ? '<div role="alertdialog" aria-labelledby="leave-title"><h3 id="leave-title">Confirm departure</h3><p>Your membership, consent and notes in this app will be removed. Your Fidj account and other apps remain available.</p><button id="confirm-leave" class="danger">Leave &amp; erase</button><button id="cancel-leave">Keep my membership</button></div>' : '<button id="leave" class="danger">Leave &amp; erase</button>'}<p class="fineprint">Exports here include your Fidj membership and this starter’s notes. The registered app-data handler lets Fidj export and erase these notes too. If cleanup is pending, retry from My privacy on Fidj. Minimal completion receipts are retained; backups and unregistered systems are outside this operation.</p></article></section>`
227
+ : `<section class="card privacy-card">${memberCard({
228
+ consent: privacy?.consent || {},
229
+ history: privacy?.history || [],
230
+ agreementHref: privacy?.consent.termsVersion
231
+ ? `${settings.apiEndpoint}/apps/${encodeURIComponent(settings.appId)}/agreements/${encodeURIComponent(String(privacy.consent.termsVersion))}`
232
+ : undefined,
233
+ owner: session.roles.includes("Owner"),
234
+ leaving,
235
+ leaveScope:
236
+ "Your membership, your choices and your notes in this app will be erased. Your Fidj account and other apps remain available.",
237
+ manageHref: `${settings.dashboardUrl}/#/my/gdpr`,
238
+ })}</section>`
239
239
  }`
240
240
  }
241
241
  <footer>Built with Fidj · One identity. Separate choices for every app.</footer></main>`;
@@ -348,7 +348,7 @@ function render() {
348
348
  void action(async () => {
349
349
  await api("privacy", "PUT", { terms: true });
350
350
  await load();
351
- notice = "Demo agreement accepted.";
351
+ notice = "Service agreement accepted.";
352
352
  }),
353
353
  );
354
354
  el("export")?.addEventListener(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
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": {