@ofidj/generator-fidj 3.17.0 → 3.18.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.
@@ -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.1"
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";
@@ -167,7 +167,10 @@ async function request(path: string, method = "GET", data?: unknown) {
167
167
  });
168
168
  const result = await response.json();
169
169
  if (!response.ok) {
170
- if ([401, 403].includes(response.status)) {
170
+ // Only a 401 means the session is gone; a 403 refuses one action to
171
+ // somebody still signed in, and signing them out for it ended the console's
172
+ // session on the first forbidden request.
173
+ if (response.status === 401) {
171
174
  signedIn = false;
172
175
  await sdk.logout(true);
173
176
  }
@@ -412,16 +415,16 @@ function signInThroughProvider(
412
415
  void action(async () => {
413
416
  let url: string;
414
417
  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.
418
+ // Somebody who has just signed out of this app did not ask to be walked
419
+ // straight back in: the provider still knows the browser and would answer
420
+ // with a code and no screen. So the door asks which account — the consent
421
+ // screen names the one Fidj recognises and offers another. It does not ask
422
+ // for a password: prompt=login told Fidj "this is not me" and ended the
423
+ // Fidj session along with the app's.
421
424
  url = await oidc.beginLogin(
422
425
  options.prompt || options.silent || !oidc.signedOutHere()
423
426
  ? options
424
- : {...options, prompt: "login"},
427
+ : {...options, prompt: "consent"},
425
428
  );
426
429
  } catch (error) {
427
430
  providerWindow?.giveUp();
@@ -917,7 +920,7 @@ function wirePrivacy() {
917
920
  navigate("signin");
918
921
  }),
919
922
  );
920
- element("terms")?.addEventListener(
923
+ element("accept-terms")?.addEventListener(
921
924
  "click",
922
925
  () =>
923
926
  void action(async () => {
@@ -1001,36 +1004,15 @@ function accountRows() {
1001
1004
  }
1002
1005
 
1003
1006
  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>`;
1007
+ const version = String((consent as Record<string, unknown>).termsVersion || "");
1008
+ return memberCard({
1009
+ consent,
1010
+ history,
1011
+ agreementHref: `${config.apiEndpoint}/apps/${encodeURIComponent(config.appId)}/agreements/${encodeURIComponent(version)}`,
1012
+ owner: roles.includes("Owner"),
1013
+ leaving,
1014
+ manageHref: `${config.dashboardUrl}/#/my/gdpr`,
1015
+ });
1034
1016
  }
1035
1017
 
1036
1018
  // ------------------------------------------------ signing in, for the provider
@@ -1059,6 +1041,8 @@ type Interaction = {
1059
1041
  recognisedEmail?: string;
1060
1042
  // The passkey door's challenge, on the login step.
1061
1043
  passkey?: {ticket: string; options: unknown};
1044
+ // The recognised person's agreement is on file at the version in force.
1045
+ agreementAccepted?: boolean;
1062
1046
  action: string;
1063
1047
  };
1064
1048
  let interactionId = "";
@@ -1184,6 +1168,7 @@ function interactionScreen() {
1184
1168
  agreementHref: details.termsUri || undefined,
1185
1169
  recognisedEmail: details.recognisedEmail || undefined,
1186
1170
  passkey: details.passkey,
1171
+ agreementAccepted: details.agreementAccepted,
1187
1172
  logoSrc: "./fidj-logo.png",
1188
1173
  })}`;
1189
1174
  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.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": {