@ofidj/generator-fidj 1.0.8 → 1.0.9

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/README.md CHANGED
@@ -128,7 +128,7 @@ Readiness confirms this handler only. Register its trusted URL and independent s
128
128
 
129
129
  ## Beta OIDC input
130
130
 
131
- `--oidc-issuer` selects the OIDC entry for content/module apps while preserving branding and anonymous-entry configuration. This is beta: the API ships an integrated provider from 3.6.26, but it serves no `/oidc` routes until an operator configures an issuer and signing keys, so confirm the issuer you pass actually answers discovery before offering it.
131
+ `--oidc-issuer` selects the OIDC entry for any generated app — a content site, a console module, or an app with its own backend — while preserving branding and anonymous-entry configuration. The entry becomes a single **Continue with Fidj** button, no password field, and the generated `.env` carries `FIDJ_OIDC_ISSUER` so the app's own server can serve it to its page. Register the app's exact callback first (`PUT /v3/apps/:appId/oidc`), or the provider refuses the authorization. This is beta: the API ships an integrated provider from 3.6.26, but it serves no `/oidc` routes until an operator configures an issuer and signing keys, so confirm the issuer you pass actually answers discovery before offering it.
132
132
 
133
133
  A compatible issuer, REST API and registered callback without a fragment are prerequisites. Validate that integration and coordinated SDK publication before offering it to app builders. Notes retains its existing authentication flow; no subject migration is implied.
134
134
 
@@ -320,9 +320,16 @@ if (require.main === module) {
320
320
  throw new Error("Invalid Fidj API URL.");
321
321
  const port = Number(process.env.PORT || 8200);
322
322
  const host = process.env.HOST || "127.0.0.1";
323
+ // The page needs the issuer to start a redirect sign-in, and it may only be
324
+ // this API's own provider: an issuer on another origin would send people to
325
+ // type this app's credentials somewhere unrelated.
326
+ const oidcIssuer = process.env.FIDJ_OIDC_ISSUER || "";
327
+ if (oidcIssuer && new URL(oidcIssuer).href !== new URL("/oidc", api).href)
328
+ throw new Error("FIDJ_OIDC_ISSUER must be the Fidj API's own /oidc endpoint.");
323
329
  const settings = {
324
330
  appId,
325
331
  apiEndpoint,
332
+ oidcIssuer,
326
333
  dashboardUrl: process.env.FIDJ_DASHBOARD_URL || "https://fidj.ovh",
327
334
  title: process.env.APP_TITLE || "My workspace",
328
335
  releaseVersion: process.env.APP_VERSION || "",
@@ -1,5 +1,5 @@
1
1
  import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
2
- import { FidjNodeService } from "@ofidj/node";
2
+ import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
3
3
  import "./style.css";
4
4
  import { showVersionBadge } from "./version";
5
5
 
@@ -12,9 +12,13 @@ type Settings = {
12
12
  dashboardUrl: string;
13
13
  localDemo: boolean;
14
14
  releaseVersion: string;
15
+ oidcIssuer?: string;
15
16
  };
16
17
  const root = document.querySelector<HTMLDivElement>("#app")!;
17
18
  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.
21
+ let oidc: FidjOidcClient | null = null;
18
22
  let settings: Settings;
19
23
  let session: Session | null = null;
20
24
  let notes: Note[] = [];
@@ -90,7 +94,7 @@ function render() {
90
94
  <main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
91
95
  ${
92
96
  !session
93
- ? `<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><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>`
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 ? `<p>Continue securely with your Fidj account. Your password stays with Fidj.</p>${agreementMarkup()}<button class="primary" type="submit">Continue with Fidj</button>` : `<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>`
94
98
  : `
95
99
  <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>
96
100
  <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>
@@ -115,8 +119,8 @@ function render() {
115
119
  void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId, signInAgreementAccepted);
116
120
  el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
117
121
  event.preventDefault();
118
- const email = el<HTMLInputElement>("email").value;
119
- const password = el<HTMLInputElement>("password").value;
122
+ const email = el<HTMLInputElement>("email")?.value || "";
123
+ const password = el<HTMLInputElement>("password")?.value || "";
120
124
  signInEmail = email;
121
125
  signInPassword = password;
122
126
  signInAgreementAccepted = el<HTMLInputElement>("service-agreement").checked;
@@ -127,6 +131,10 @@ function render() {
127
131
  return;
128
132
  }
129
133
  void action(async () => {
134
+ if (oidc) {
135
+ window.location.assign(await oidc.beginLogin());
136
+ return;
137
+ }
130
138
  try {
131
139
  await sdk.login(email, password, { autoSignup: false, ...acceptance });
132
140
  } catch (reason) {
@@ -251,6 +259,21 @@ async function start() {
251
259
  try {
252
260
  settings = await (await fetch("/api/config")).json();
253
261
  showVersionBadge(settings.releaseVersion, settings.title === "Fidj" ? settings.apiEndpoint : undefined);
262
+ if (settings.oidcIssuer)
263
+ oidc = new FidjOidcClient({
264
+ issuer: settings.oidcIssuer,
265
+ clientId: settings.appId,
266
+ redirectUri: window.location.origin + window.location.pathname,
267
+ apiEndpoint: settings.apiEndpoint,
268
+ storage: sessionStorage,
269
+ });
270
+ // Coming back from Fidj: swap the code for a session, and take the code out
271
+ // of the address bar before anything can reload it.
272
+ if (oidc && new URL(window.location.href).searchParams.has("state")) {
273
+ const callback = new URL(window.location.href);
274
+ window.history.replaceState(null, "", window.location.pathname);
275
+ await oidc.completeLogin(callback);
276
+ }
254
277
  await sdk.init(settings.appId, {
255
278
  apiEndpoint: settings.apiEndpoint,
256
279
  prod: !settings.localDemo,
package/lib/scaffold.cjs CHANGED
@@ -36,7 +36,6 @@ function scaffold(destination, options) {
36
36
  if (options.oidcIssuer) {
37
37
  const issuer = new URL(options.oidcIssuer);
38
38
  if (issuer.origin !== api.origin || issuer.pathname !== "/oidc" || issuer.hash || issuer.search || issuer.username || issuer.password) throw new Error("OIDC issuer must share the API origin and use /oidc.");
39
- if (options.content === undefined && !options.module) throw new Error("OIDC currently requires a content app or console module.");
40
39
  }
41
40
  const title = options.title || name;
42
41
  if (/[\r\n]/.test(title)) throw new Error("Title must be one line.");
@@ -127,7 +126,10 @@ function scaffold(destination, options) {
127
126
  );
128
127
  fs.renameSync(path.join(dir, "gitignore"), path.join(dir, ".gitignore"));
129
128
  const releaseVersion = new Date().toISOString().slice(2, 10).replaceAll("-", ".");
130
- const settings = `FIDJ_APP_ID=${options.appId}\nFIDJ_API_ENDPOINT=${api.href.replace(/\/$/, "")}\nFIDJ_DASHBOARD_URL=https://fidj.ovh\nAPP_TITLE=${title}\nAPP_VERSION=${releaseVersion}\nPORT=8200\nHOST=127.0.0.1\nLOCAL_DEMO=false\nFIDJ_DATA_DIR=../.fidj-data/${name}\nFIDJ_PRIVACY_ADAPTER_KEY=\n`;
129
+ // The app's own server serves this to its page: the entry cannot redirect to
130
+ // a provider it was never told about.
131
+ const issuerSetting = options.oidcIssuer ? `FIDJ_OIDC_ISSUER=${options.oidcIssuer}\n` : "";
132
+ const settings = `FIDJ_APP_ID=${options.appId}\nFIDJ_API_ENDPOINT=${api.href.replace(/\/$/, "")}\n${issuerSetting}FIDJ_DASHBOARD_URL=https://fidj.ovh\nAPP_TITLE=${title}\nAPP_VERSION=${releaseVersion}\nPORT=8200\nHOST=127.0.0.1\nLOCAL_DEMO=false\nFIDJ_DATA_DIR=../.fidj-data/${name}\nFIDJ_PRIVACY_ADAPTER_KEY=\n`;
131
133
  const localApi = options.local
132
134
  ? "http://localhost:3201/v3"
133
135
  : api.href.replace(/\/$/, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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": {