@scalemule/news-publication 0.0.88 → 0.0.90

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
@@ -249,7 +249,7 @@ The consent banner now offers a choice in every geography where optional measure
249
249
  ## Reader sign-in through Ledvery
250
250
 
251
251
  The `reader-sso` (server) and `reader-sso-client` entry points generalize the
252
- publications' Google sign-in into one Ledvery consumer flow with silent sign-in,
252
+ publications' Google sign-in into one Ledvery consumer flow with click-driven handoff,
253
253
  explicit email sign-in, account linking, precedence between two accounts,
254
254
  RP-initiated sign-out and server-recorded telemetry. A site supplies its cookie
255
255
  prefix, canonical origin, Ledvery client id and a request adapter that performs
@@ -262,7 +262,13 @@ them with `applyCookies` and answers with private, no-store headers. Merge
262
262
  `READER_SSO_EVENTS` into the site's analytics registry and allow
263
263
  `READER_SSO_PROPERTY_KEYS` in its property sanitizer. `ReaderSsoBoot` is a
264
264
  no-op compatibility export: a rendered page never starts Ledvery authorization
265
- on a timer. The explicit sign-in routes still work. `ReaderGreeting` is a client
265
+ on a timer. Ordinary explicit sign-in omits `prompt`; `select_account` is reserved
266
+ for an explicit account-switch control and `prompt=none` for a sister-paper
267
+ handoff. Handoff links carry no identity or session token, and
268
+ `networkHandoffReturnPath` discards caller-selected destinations. The two
269
+ initial canaries are listed by `getNetworkReaderHandoffUrl`; other papers
270
+ remain plain links until their route and client registration are verified.
271
+ `ReaderGreeting` is a client
266
272
  island that renders only a first name, in the reader's own time zone and locale,
267
273
  after loading.
268
274
 
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { setupNetworkConsentDelegation, COOKIE_CONSENT_EVENT, listedPublications, decorateNetworkUrl } from './chunk-XWXPERLK.js';
2
+ import { setupNetworkConsentDelegation, COOKIE_CONSENT_EVENT, listedPublications, decorateNetworkUrl } from './chunk-NNEY6VBX.js';
3
3
  import { useState, useId, useRef, useEffect } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
5
 
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { hasConsented, rejectOptionalCookies, DEFAULT_CONSENT_HUB_URL, consentModel, isGpcActive, COOKIE_CONSENT_EVENT, acceptAllCookies, recordConsentToCompliance } from './chunk-XWXPERLK.js';
2
+ import { hasConsented, rejectOptionalCookies, DEFAULT_CONSENT_HUB_URL, consentModel, isGpcActive, COOKIE_CONSENT_EVENT, acceptAllCookies, recordConsentToCompliance } from './chunk-NNEY6VBX.js';
3
3
  import { useState, useEffect } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
@@ -1,4 +1,4 @@
1
- import { NETWORK_PARENT } from './chunk-XWXPERLK.js';
1
+ import { NETWORK_PARENT } from './chunk-NNEY6VBX.js';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  function NetworkCopyrightNotice({
@@ -368,6 +368,10 @@ function getNetworkPublicationUrl(slug) {
368
368
  if (!pub) return null;
369
369
  return decorateNetworkUrl(pub.url + "/");
370
370
  }
371
+ function getNetworkReaderHandoffUrl(slug) {
372
+ const publication = findPublicationBySlug(slug);
373
+ return publication ? `${publication.url}/api/news/network-handoff` : null;
374
+ }
371
375
  var NETWORK_HOSTNAMES = new Set(
372
376
  localPublications.map((p) => {
373
377
  try {
@@ -383,4 +387,4 @@ function isNetworkHostname(hostname) {
383
387
  return clean === "bayareanewsnetwork.com" || clean.endsWith(".bayareanewsnetwork.com") || NETWORK_HOSTNAMES.has(clean) || clean.endsWith(".bayareachronicle.com") || clean.endsWith(".napsite.com") || clean.endsWith(".site.scalemule.com");
384
388
  }
385
389
 
386
- export { COOKIE_CATEGORIES, EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, recordConsentToCompliance, setupNetworkConsentDelegation, topicalPublications };
390
+ export { COOKIE_CATEGORIES, EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, isNetworkHostname, listedPublications, localPublications, recordConsentToCompliance, setupNetworkConsentDelegation, topicalPublications };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { effectivePreferences, getCookieConsent, COOKIE_CATEGORIES, setCookieConsent, recordConsentToCompliance, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED } from './chunk-XWXPERLK.js';
2
+ import { effectivePreferences, getCookieConsent, COOKIE_CATEGORIES, setCookieConsent, recordConsentToCompliance, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED } from './chunk-NNEY6VBX.js';
3
3
  import { useState, useEffect } from 'react';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
5
 
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- export { CookieConsentBanner } from './chunk-XSAVJJVW.js';
3
- import './chunk-XWXPERLK.js';
2
+ export { CookieConsentBanner } from './chunk-D27NMCRK.js';
3
+ import './chunk-NNEY6VBX.js';
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- export { CookieSettingsView } from './chunk-X7AU7DV6.js';
3
- import './chunk-XWXPERLK.js';
2
+ export { CookieSettingsView } from './chunk-PBRDIXQI.js';
3
+ import './chunk-NNEY6VBX.js';
package/dist/index.cjs CHANGED
@@ -1477,6 +1477,10 @@ function getNetworkPublicationUrl(slug) {
1477
1477
  if (!pub) return null;
1478
1478
  return compliance.decorateNetworkUrl(pub.url + "/");
1479
1479
  }
1480
+ function getNetworkReaderHandoffUrl(slug) {
1481
+ const publication = findPublicationBySlug(slug);
1482
+ return publication ? `${publication.url}/api/news/network-handoff` : null;
1483
+ }
1480
1484
  var NETWORK_HOSTNAMES = new Set(
1481
1485
  localPublications.map((p) => {
1482
1486
  try {
@@ -4001,6 +4005,7 @@ exports.findPublicationBySlug = findPublicationBySlug;
4001
4005
  exports.formatArtworkCaption = formatArtworkCaption;
4002
4006
  exports.getArticleCards = getArticleCards;
4003
4007
  exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
4008
+ exports.getNetworkReaderHandoffUrl = getNetworkReaderHandoffUrl;
4004
4009
  exports.getStoredTheme = getStoredTheme;
4005
4010
  exports.getSystemTheme = getSystemTheme;
4006
4011
  exports.inventoryCacheOptions = inventoryCacheOptions;
package/dist/index.d.cts CHANGED
@@ -8,7 +8,7 @@ export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CO
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
9
9
  export { CookieSettingsView, CookieSettingsViewProps } from './cookie-settings-view.cjs';
10
10
  export { ThemeToggle, ThemeToggleProps } from './theme-toggle.cjs';
11
- export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications, topicalPublications } from './network.cjs';
11
+ export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, listedPublications, localPublications, topicalPublications } from './network.cjs';
12
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.cjs';
13
13
  export { NetworkCopyrightNotice, NetworkCopyrightNoticeProps } from './network-copyright.cjs';
14
14
  import * as React from 'react';
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CO
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.js';
9
9
  export { CookieSettingsView, CookieSettingsViewProps } from './cookie-settings-view.js';
10
10
  export { ThemeToggle, ThemeToggleProps } from './theme-toggle.js';
11
- export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications, topicalPublications } from './network.js';
11
+ export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, listedPublications, localPublications, topicalPublications } from './network.js';
12
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.js';
13
13
  export { NetworkCopyrightNotice, NetworkCopyrightNoticeProps } from './network-copyright.js';
14
14
  import * as React from 'react';
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- export { LocalNetworkBar } from './chunk-Q77T2S7B.js';
2
- export { NetworkCopyrightNotice } from './chunk-FWEYKWKP.js';
1
+ export { LocalNetworkBar } from './chunk-2E42LCN3.js';
2
+ export { NetworkCopyrightNotice } from './chunk-FJ672GWO.js';
3
3
  export { CURATED_COMMUNITY_CARDS, VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-KYUVXEJY.js';
4
4
  export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedPath, classifiedSearchPath, clipChars, dealDollars, dealGoPath, dealsFeedPath, employmentLabel, isClassifiedCategory, isEmploymentType, jobEmployerBody, jobOpeningBody, jobPostingBody, prepareClassified, prepareJobPost, publicJobPath, publicJobsPath } from './chunk-C7ZPLH2K.js';
5
5
  export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-AMLVCZDP.js';
6
- export { CookieConsentBanner } from './chunk-XSAVJJVW.js';
7
- export { CookieSettingsView } from './chunk-X7AU7DV6.js';
8
- import { localPublications } from './chunk-XWXPERLK.js';
9
- export { COOKIE_CATEGORIES, COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EDITORIAL_PRINCIPLES, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, NETWORK_PARENT, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, communityPublications, consentModel, decorateNetworkUrl, effectivePreferences, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub, topicalPublications } from './chunk-XWXPERLK.js';
6
+ export { CookieConsentBanner } from './chunk-D27NMCRK.js';
7
+ export { CookieSettingsView } from './chunk-PBRDIXQI.js';
8
+ import { localPublications } from './chunk-NNEY6VBX.js';
9
+ export { COOKIE_CATEGORIES, COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EDITORIAL_PRINCIPLES, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, NETWORK_PARENT, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, communityPublications, consentModel, decorateNetworkUrl, effectivePreferences, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub, topicalPublications } from './chunk-NNEY6VBX.js';
10
10
  export { THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, ThemeToggle, applyTheme, clearTheme, getStoredTheme, getSystemTheme, resolveEffectiveTheme, setTheme, subscribeTheme, themeInitScript } from './chunk-NQD3M7ZY.js';
11
11
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
12
12
  export { articleFormatLabel, articleHeroRatio, articlePresentation, formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-DN3TBZLK.js';
@@ -1,2 +1,2 @@
1
- export { LocalNetworkBar } from './chunk-Q77T2S7B.js';
2
- import './chunk-XWXPERLK.js';
1
+ export { LocalNetworkBar } from './chunk-2E42LCN3.js';
2
+ import './chunk-NNEY6VBX.js';
@@ -1,2 +1,2 @@
1
- export { NetworkCopyrightNotice } from './chunk-FWEYKWKP.js';
2
- import './chunk-XWXPERLK.js';
1
+ export { NetworkCopyrightNotice } from './chunk-FJ672GWO.js';
2
+ import './chunk-NNEY6VBX.js';
package/dist/network.cjs CHANGED
@@ -245,6 +245,10 @@ function getNetworkPublicationUrl(slug) {
245
245
  if (!pub) return null;
246
246
  return compliance.decorateNetworkUrl(pub.url + "/");
247
247
  }
248
+ function getNetworkReaderHandoffUrl(slug) {
249
+ const publication = findPublicationBySlug(slug);
250
+ return publication ? `${publication.url}/api/news/network-handoff` : null;
251
+ }
248
252
  var NETWORK_HOSTNAMES = new Set(
249
253
  localPublications.map((p) => {
250
254
  try {
@@ -266,6 +270,7 @@ exports.communityPublications = communityPublications;
266
270
  exports.findPublicationByHost = findPublicationByHost;
267
271
  exports.findPublicationBySlug = findPublicationBySlug;
268
272
  exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
273
+ exports.getNetworkReaderHandoffUrl = getNetworkReaderHandoffUrl;
269
274
  exports.isNetworkHostname = isNetworkHostname;
270
275
  exports.listedPublications = listedPublications;
271
276
  exports.localPublications = localPublications;
@@ -58,7 +58,13 @@ declare function findPublicationBySlug(slug: string): PublicationNetworkItem | u
58
58
  declare function findPublicationByHost(host: string): PublicationNetworkItem | undefined;
59
59
  /** Get decorated URL for cross-domain navigation within the network with consent transfer. */
60
60
  declare function getNetworkPublicationUrl(slug: string): string | null;
61
+ /**
62
+ * The fixed destination route for a publication in this reviewed network list.
63
+ * Sites only use it when their handoff switch is enabled after every destination
64
+ * has deployed the route. No identity or caller-selected return URL is carried.
65
+ */
66
+ declare function getNetworkReaderHandoffUrl(slug: string): string | null;
61
67
  /** Check whether a given hostname belongs to any publication in the ScaleMule news network. */
62
68
  declare function isNetworkHostname(hostname: string): boolean;
63
69
 
64
- export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
70
+ export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
package/dist/network.d.ts CHANGED
@@ -58,7 +58,13 @@ declare function findPublicationBySlug(slug: string): PublicationNetworkItem | u
58
58
  declare function findPublicationByHost(host: string): PublicationNetworkItem | undefined;
59
59
  /** Get decorated URL for cross-domain navigation within the network with consent transfer. */
60
60
  declare function getNetworkPublicationUrl(slug: string): string | null;
61
+ /**
62
+ * The fixed destination route for a publication in this reviewed network list.
63
+ * Sites only use it when their handoff switch is enabled after every destination
64
+ * has deployed the route. No identity or caller-selected return URL is carried.
65
+ */
66
+ declare function getNetworkReaderHandoffUrl(slug: string): string | null;
61
67
  /** Check whether a given hostname belongs to any publication in the ScaleMule news network. */
62
68
  declare function isNetworkHostname(hostname: string): boolean;
63
69
 
64
- export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
70
+ export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
package/dist/network.js CHANGED
@@ -1 +1 @@
1
- export { EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications } from './chunk-XWXPERLK.js';
1
+ export { EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications } from './chunk-NNEY6VBX.js';
@@ -84,6 +84,9 @@ function localReturnPath(value) {
84
84
  if (/[\\\r\n\u0000-\u001f]/.test(value) || value === "/api" || value.startsWith("/api/")) return "";
85
85
  return value;
86
86
  }
87
+ function networkHandoffReturnPath(query) {
88
+ return query.getAll("sm_consent").length === 1 && query.get("sm_consent") === "0" ? "/?sm_consent=0" : "/";
89
+ }
87
90
  function resolve(config) {
88
91
  const origin = new URL(config.origin);
89
92
  if (origin.protocol !== "https:" || origin.origin !== config.origin) throw new Error("Reader SSO origin must be a bare https origin");
@@ -177,9 +180,17 @@ function openPendingSession(cookie, key, now = Date.now()) {
177
180
  tx: typeof raw.tx === "string" ? raw.tx : void 0
178
181
  };
179
182
  }
180
- function authorizeUrl(value, state, callback, mode, issuerOrigin = LEDVERY_ISSUER_ORIGIN) {
183
+ function authorizeUrl(value, state, callback, mode, issuerOrigin = LEDVERY_ISSUER_ORIGIN, selectAccount = false, clientId) {
181
184
  const url = new URL(value);
182
- if (url.protocol !== "https:" || url.origin !== new URL(issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.searchParams.get("state") !== state || url.searchParams.get("redirect_uri") !== callback || url.searchParams.get("code_challenge_method") !== "S256" || !url.searchParams.get("code_challenge") || mode === "silent" && url.searchParams.get("prompt") !== "none") throw new Error("Invalid Ledvery authorization response");
185
+ const prompts = url.searchParams.getAll("prompt");
186
+ const expectedPrompt = mode === "silent" ? "none" : selectAccount ? "select_account" : null;
187
+ const exactly = (key, expected) => {
188
+ const values = url.searchParams.getAll(key);
189
+ return values.length === 1 && values[0] === expected;
190
+ };
191
+ const scope = url.searchParams.getAll("scope");
192
+ const nonce = url.searchParams.getAll("nonce");
193
+ if (url.protocol !== "https:" || url.origin !== new URL(issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.hash || !exactly("state", state) || !exactly("redirect_uri", callback) || !exactly("response_type", "code") || clientId !== void 0 && !exactly("client_id", clientId) || scope.length !== 1 || !scope[0].split(/\s+/).includes("openid") || nonce.length !== 1 || !nonce[0] || !exactly("code_challenge_method", "S256") || url.searchParams.getAll("code_challenge").length !== 1 || !url.searchParams.get("code_challenge") || (expectedPrompt === null ? prompts.length !== 0 : prompts.length !== 1 || prompts[0] !== expectedPrompt)) throw new Error("Invalid Ledvery authorization response");
183
194
  return url.toString();
184
195
  }
185
196
  async function startSignIn(config, input) {
@@ -213,7 +224,7 @@ async function startSignIn(config, input) {
213
224
  const query = new URLSearchParams({ redirect_uri: callback });
214
225
  if (mode === "silent") query.set("prompt", "none");
215
226
  else {
216
- query.set("prompt", "select_account");
227
+ if (input.selectAccount === true) query.set("prompt", "select_account");
217
228
  query.set("upstream", provider === "email" ? "scalemule_consumer" : "google");
218
229
  }
219
230
  try {
@@ -221,7 +232,7 @@ async function startSignIn(config, input) {
221
232
  if (failed(response)) throw new Error("authorize");
222
233
  const value = payload(response);
223
234
  if (typeof value.url !== "string" || typeof value.state !== "string" || !value.state) throw new Error("authorize");
224
- const url = authorizeUrl(value.url, value.state, callback, mode, c.issuerOrigin);
235
+ const url = authorizeUrl(value.url, value.state, callback, mode, c.issuerOrigin, input.selectAccount === true, c.clientId);
225
236
  const cookies = [
226
237
  ...staleSession,
227
238
  { name: c.names.flow, value: sealReaderFlow({ state: value.state, subscribe: input.subscribe === true && mode === "interactive", returnTo: next, mode, provider, tx }, c.key, c.now()), maxAge: READER_SSO_FLOW_SECONDS }
@@ -537,6 +548,7 @@ exports.cookieReader = cookieReader;
537
548
  exports.linkAccount = linkAccount;
538
549
  exports.linkFailureMessage = linkFailureMessage;
539
550
  exports.localReturnPath = localReturnPath;
551
+ exports.networkHandoffReturnPath = networkHandoffReturnPath;
540
552
  exports.openPendingSession = openPendingSession;
541
553
  exports.openReaderFlow = openReaderFlow;
542
554
  exports.pendingSwitch = pendingSwitch;
@@ -132,6 +132,12 @@ type ReaderSsoConfig = {
132
132
  };
133
133
  /** Any same-site path: no protocol-relative URLs, control characters or API routes. */
134
134
  declare function localReturnPath(value: unknown): string;
135
+ /**
136
+ * A network-bar handoff always lands on this paper's home page. The only
137
+ * transferable query value is the existing refusal of optional cookies.
138
+ * Identity, callback and return destinations never come from the link.
139
+ */
140
+ declare function networkHandoffReturnPath(query: URLSearchParams): string;
135
141
  declare function stateDigest(state: string): string;
136
142
  declare function validState(state: string | null | undefined, digest: string | undefined): boolean;
137
143
  type ReaderSsoFlow = {
@@ -166,10 +172,12 @@ type PendingSession = {
166
172
  };
167
173
  declare function sealPendingSession(pending: PendingSession, key: string, now?: number): string;
168
174
  declare function openPendingSession(cookie: string | undefined, key: string, now?: number): PendingSession | null;
169
- declare function authorizeUrl(value: string, state: string, callback: string, mode: ReaderSsoMode, issuerOrigin?: string): string;
175
+ declare function authorizeUrl(value: string, state: string, callback: string, mode: ReaderSsoMode, issuerOrigin?: string, selectAccount?: boolean, clientId?: string): string;
170
176
  type StartSignInInput = {
171
177
  mode: ReaderSsoMode;
172
178
  provider?: ReaderSsoProvider;
179
+ /** Only an explicit Switch account control may request the account chooser. */
180
+ selectAccount?: boolean;
173
181
  subscribe?: boolean;
174
182
  next?: string;
175
183
  cookies: CookieReader;
@@ -319,4 +327,4 @@ declare function readerSsoBootFlags(cookies: CookieReader, names: ReaderSsoCooki
319
327
  suppressed: boolean;
320
328
  };
321
329
 
322
- export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, type ReaderSsoCheckOutcome, type ReaderSsoConfig, type ReaderSsoCookieNames, type ReaderSsoEvent, type ReaderSsoFailure, type ReaderSsoFlow, type ReaderSsoMethod, type ReaderSsoMode, type ReaderSsoProvider, type ReaderSsoRequest, type ReaderSsoResponse, SILENT_NEGATIVE_ERRORS, type SessionSummary, type SessionUser, type SignOutResult, type StartSignInInput, type StartSignInResult, type SwitchAccountResult, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
330
+ export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, type ReaderSsoCheckOutcome, type ReaderSsoConfig, type ReaderSsoCookieNames, type ReaderSsoEvent, type ReaderSsoFailure, type ReaderSsoFlow, type ReaderSsoMethod, type ReaderSsoMode, type ReaderSsoProvider, type ReaderSsoRequest, type ReaderSsoResponse, SILENT_NEGATIVE_ERRORS, type SessionSummary, type SessionUser, type SignOutResult, type StartSignInInput, type StartSignInResult, type SwitchAccountResult, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, networkHandoffReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
@@ -132,6 +132,12 @@ type ReaderSsoConfig = {
132
132
  };
133
133
  /** Any same-site path: no protocol-relative URLs, control characters or API routes. */
134
134
  declare function localReturnPath(value: unknown): string;
135
+ /**
136
+ * A network-bar handoff always lands on this paper's home page. The only
137
+ * transferable query value is the existing refusal of optional cookies.
138
+ * Identity, callback and return destinations never come from the link.
139
+ */
140
+ declare function networkHandoffReturnPath(query: URLSearchParams): string;
135
141
  declare function stateDigest(state: string): string;
136
142
  declare function validState(state: string | null | undefined, digest: string | undefined): boolean;
137
143
  type ReaderSsoFlow = {
@@ -166,10 +172,12 @@ type PendingSession = {
166
172
  };
167
173
  declare function sealPendingSession(pending: PendingSession, key: string, now?: number): string;
168
174
  declare function openPendingSession(cookie: string | undefined, key: string, now?: number): PendingSession | null;
169
- declare function authorizeUrl(value: string, state: string, callback: string, mode: ReaderSsoMode, issuerOrigin?: string): string;
175
+ declare function authorizeUrl(value: string, state: string, callback: string, mode: ReaderSsoMode, issuerOrigin?: string, selectAccount?: boolean, clientId?: string): string;
170
176
  type StartSignInInput = {
171
177
  mode: ReaderSsoMode;
172
178
  provider?: ReaderSsoProvider;
179
+ /** Only an explicit Switch account control may request the account chooser. */
180
+ selectAccount?: boolean;
173
181
  subscribe?: boolean;
174
182
  next?: string;
175
183
  cookies: CookieReader;
@@ -319,4 +327,4 @@ declare function readerSsoBootFlags(cookies: CookieReader, names: ReaderSsoCooki
319
327
  suppressed: boolean;
320
328
  };
321
329
 
322
- export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, type ReaderSsoCheckOutcome, type ReaderSsoConfig, type ReaderSsoCookieNames, type ReaderSsoEvent, type ReaderSsoFailure, type ReaderSsoFlow, type ReaderSsoMethod, type ReaderSsoMode, type ReaderSsoProvider, type ReaderSsoRequest, type ReaderSsoResponse, SILENT_NEGATIVE_ERRORS, type SessionSummary, type SessionUser, type SignOutResult, type StartSignInInput, type StartSignInResult, type SwitchAccountResult, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
330
+ export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, type ReaderSsoCheckOutcome, type ReaderSsoConfig, type ReaderSsoCookieNames, type ReaderSsoEvent, type ReaderSsoFailure, type ReaderSsoFlow, type ReaderSsoMethod, type ReaderSsoMode, type ReaderSsoProvider, type ReaderSsoRequest, type ReaderSsoResponse, SILENT_NEGATIVE_ERRORS, type SessionSummary, type SessionUser, type SignOutResult, type StartSignInInput, type StartSignInResult, type SwitchAccountResult, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, networkHandoffReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
@@ -69,6 +69,9 @@ function localReturnPath(value) {
69
69
  if (/[\\\r\n\u0000-\u001f]/.test(value) || value === "/api" || value.startsWith("/api/")) return "";
70
70
  return value;
71
71
  }
72
+ function networkHandoffReturnPath(query) {
73
+ return query.getAll("sm_consent").length === 1 && query.get("sm_consent") === "0" ? "/?sm_consent=0" : "/";
74
+ }
72
75
  function resolve(config) {
73
76
  const origin = new URL(config.origin);
74
77
  if (origin.protocol !== "https:" || origin.origin !== config.origin) throw new Error("Reader SSO origin must be a bare https origin");
@@ -162,9 +165,17 @@ function openPendingSession(cookie, key, now = Date.now()) {
162
165
  tx: typeof raw.tx === "string" ? raw.tx : void 0
163
166
  };
164
167
  }
165
- function authorizeUrl(value, state, callback, mode, issuerOrigin = LEDVERY_ISSUER_ORIGIN) {
168
+ function authorizeUrl(value, state, callback, mode, issuerOrigin = LEDVERY_ISSUER_ORIGIN, selectAccount = false, clientId) {
166
169
  const url = new URL(value);
167
- if (url.protocol !== "https:" || url.origin !== new URL(issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.searchParams.get("state") !== state || url.searchParams.get("redirect_uri") !== callback || url.searchParams.get("code_challenge_method") !== "S256" || !url.searchParams.get("code_challenge") || mode === "silent" && url.searchParams.get("prompt") !== "none") throw new Error("Invalid Ledvery authorization response");
170
+ const prompts = url.searchParams.getAll("prompt");
171
+ const expectedPrompt = mode === "silent" ? "none" : selectAccount ? "select_account" : null;
172
+ const exactly = (key, expected) => {
173
+ const values = url.searchParams.getAll(key);
174
+ return values.length === 1 && values[0] === expected;
175
+ };
176
+ const scope = url.searchParams.getAll("scope");
177
+ const nonce = url.searchParams.getAll("nonce");
178
+ if (url.protocol !== "https:" || url.origin !== new URL(issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.hash || !exactly("state", state) || !exactly("redirect_uri", callback) || !exactly("response_type", "code") || clientId !== void 0 && !exactly("client_id", clientId) || scope.length !== 1 || !scope[0].split(/\s+/).includes("openid") || nonce.length !== 1 || !nonce[0] || !exactly("code_challenge_method", "S256") || url.searchParams.getAll("code_challenge").length !== 1 || !url.searchParams.get("code_challenge") || (expectedPrompt === null ? prompts.length !== 0 : prompts.length !== 1 || prompts[0] !== expectedPrompt)) throw new Error("Invalid Ledvery authorization response");
168
179
  return url.toString();
169
180
  }
170
181
  async function startSignIn(config, input) {
@@ -198,7 +209,7 @@ async function startSignIn(config, input) {
198
209
  const query = new URLSearchParams({ redirect_uri: callback });
199
210
  if (mode === "silent") query.set("prompt", "none");
200
211
  else {
201
- query.set("prompt", "select_account");
212
+ if (input.selectAccount === true) query.set("prompt", "select_account");
202
213
  query.set("upstream", provider === "email" ? "scalemule_consumer" : "google");
203
214
  }
204
215
  try {
@@ -206,7 +217,7 @@ async function startSignIn(config, input) {
206
217
  if (failed(response)) throw new Error("authorize");
207
218
  const value = payload(response);
208
219
  if (typeof value.url !== "string" || typeof value.state !== "string" || !value.state) throw new Error("authorize");
209
- const url = authorizeUrl(value.url, value.state, callback, mode, c.issuerOrigin);
220
+ const url = authorizeUrl(value.url, value.state, callback, mode, c.issuerOrigin, input.selectAccount === true, c.clientId);
210
221
  const cookies = [
211
222
  ...staleSession,
212
223
  { name: c.names.flow, value: sealReaderFlow({ state: value.state, subscribe: input.subscribe === true && mode === "interactive", returnTo: next, mode, provider, tx }, c.key, c.now()), maxAge: READER_SSO_FLOW_SECONDS }
@@ -507,4 +518,4 @@ function readerSsoBootFlags(cookies, names) {
507
518
  };
508
519
  }
509
520
 
510
- export { LEDVERY_ISSUER_ORIGIN, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, SILENT_NEGATIVE_ERRORS, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
521
+ export { LEDVERY_ISSUER_ORIGIN, READER_SSO_FLOW_SECONDS, READER_SSO_NEGATIVE_CACHE_SECONDS, READER_SSO_PENDING_SECONDS, READER_SSO_SIGNED_OUT_SECONDS, SILENT_NEGATIVE_ERRORS, applyCookies, authorizeUrl, completeSignIn, cookieReader, linkAccount, linkFailureMessage, localReturnPath, networkHandoffReturnPath, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "bin": {