@ofidj/generator-fidj 1.0.7 → 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
 
@@ -11,6 +11,7 @@ export interface Settings {
11
11
  apiEndpoint: string;
12
12
  dashboardUrl: string;
13
13
  title: string;
14
+ releaseVersion: string;
14
15
  localDemo: boolean;
15
16
  }
16
17
  class HttpError extends Error {
@@ -319,11 +320,19 @@ if (require.main === module) {
319
320
  throw new Error("Invalid Fidj API URL.");
320
321
  const port = Number(process.env.PORT || 8200);
321
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.");
322
329
  const settings = {
323
330
  appId,
324
331
  apiEndpoint,
332
+ oidcIssuer,
325
333
  dashboardUrl: process.env.FIDJ_DASHBOARD_URL || "https://fidj.ovh",
326
334
  title: process.env.APP_TITLE || "My workspace",
335
+ releaseVersion: process.env.APP_VERSION || "",
327
336
  localDemo:
328
337
  process.env.LOCAL_DEMO === "true" &&
329
338
  host === "127.0.0.1" &&
@@ -7,7 +7,7 @@ import { showVersionBadge } from "./version";
7
7
  const sdk = new FidjNodeService();
8
8
  const oidc = config.oidcIssuer ? new FidjOidcClient({issuer: config.oidcIssuer, clientId: config.appId, redirectUri: window.location.origin + window.location.pathname, apiEndpoint: config.apiEndpoint, storage: sessionStorage}) : null;
9
9
  const root = document.querySelector<HTMLDivElement>("#app")!;
10
- showVersionBadge(config.releaseVersion);
10
+ showVersionBadge(config.releaseVersion, config.title === "Fidj" ? config.apiEndpoint : undefined);
11
11
  const appPath = `/me/apps/${encodeURIComponent(config.appId)}`;
12
12
  let signedIn = false;
13
13
  let emailVerified = false;
@@ -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) {
@@ -250,7 +258,22 @@ function render() {
250
258
  async function start() {
251
259
  try {
252
260
  settings = await (await fetch("/api/config")).json();
253
- showVersionBadge(settings.releaseVersion);
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,
@@ -1,8 +1,19 @@
1
- export function showVersionBadge(version: string): void {
1
+ export function showVersionBadge(version: string, apiEndpoint?: string): void {
2
2
  if (!/^\d{2}\.\d{2}\.\d{2}$/.test(version || "")) return;
3
3
  const badge = document.createElement("div");
4
4
  badge.className = "fidj-version";
5
5
  badge.setAttribute("aria-label", `App version ${version}`);
6
6
  badge.textContent = `v${version}`;
7
7
  document.body.append(badge);
8
+ if (apiEndpoint) {
9
+ void fetch(`${apiEndpoint.replace(/\/$/, "")}/status`)
10
+ .then(response => response.ok ? response.json() : null)
11
+ .then(status => {
12
+ const apiVersion = status?.version || status?.built;
13
+ if (!apiVersion) return;
14
+ badge.textContent = `v${version} · API ${apiVersion}`;
15
+ badge.setAttribute("aria-label", `App version ${version}, API version ${apiVersion}`);
16
+ })
17
+ .catch(() => undefined);
18
+ }
8
19
  }
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.");
@@ -126,7 +125,11 @@ function scaffold(destination, options) {
126
125
  JSON.stringify(pkg, null, 2) + "\n",
127
126
  );
128
127
  fs.renameSync(path.join(dir, "gitignore"), path.join(dir, ".gitignore"));
129
- const settings = `FIDJ_APP_ID=${options.appId}\nFIDJ_API_ENDPOINT=${api.href.replace(/\/$/, "")}\nFIDJ_DASHBOARD_URL=https://fidj.ovh\nAPP_TITLE=${title}\nPORT=8200\nHOST=127.0.0.1\nLOCAL_DEMO=false\nFIDJ_DATA_DIR=../.fidj-data/${name}\nFIDJ_PRIVACY_ADAPTER_KEY=\n`;
128
+ const releaseVersion = new Date().toISOString().slice(2, 10).replaceAll("-", ".");
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`;
130
133
  const localApi = options.local
131
134
  ? "http://localhost:3201/v3"
132
135
  : api.href.replace(/\/$/, "");
@@ -136,7 +139,7 @@ function scaffold(destination, options) {
136
139
  apiEndpoint: localApi,
137
140
  dashboardUrl: options.local ? "http://localhost:4200" : "https://fidj.ovh",
138
141
  title,
139
- releaseVersion: new Date().toISOString().slice(2, 10).replaceAll("-", "."),
142
+ releaseVersion,
140
143
  localDemo: Boolean(options.local),
141
144
  allowAnonymous: anonymous === true || anonymous === "true",
142
145
  welcome: options.welcome || title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "1.0.7",
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": {