@scalemule/news-publication 0.0.87 → 0.0.89
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 +12 -6
- package/dist/{chunk-XWXPERLK.js → chunk-CKTTYAZY.js} +6 -1
- package/dist/{chunk-XSAVJJVW.js → chunk-EPAFIBBS.js} +1 -1
- package/dist/{chunk-Q77T2S7B.js → chunk-F6HV2HRP.js} +1 -1
- package/dist/{chunk-X7AU7DV6.js → chunk-R6WTBTXO.js} +1 -1
- package/dist/{chunk-FWEYKWKP.js → chunk-RSKB5NNE.js} +1 -1
- package/dist/cookie-consent-banner.js +2 -2
- package/dist/cookie-settings-view.js +2 -2
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/network-bar.js +2 -2
- package/dist/network-copyright.js +2 -2
- package/dist/network.cjs +6 -0
- package/dist/network.d.cts +3 -1
- package/dist/network.d.ts +3 -1
- package/dist/network.js +1 -1
- package/dist/reader-sso-client.cjs +1 -106
- package/dist/reader-sso-client.d.cts +7 -5
- package/dist/reader-sso-client.d.ts +7 -5
- package/dist/reader-sso-client.js +1 -106
- package/dist/reader-sso.cjs +16 -4
- package/dist/reader-sso.d.cts +10 -2
- package/dist/reader-sso.d.ts +10 -2
- package/dist/reader-sso.js +16 -5
- package/package.json +1 -1
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
|
|
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
|
|
@@ -260,11 +260,17 @@ other than that key and the browser only ever sees host-only HttpOnly cookies.
|
|
|
260
260
|
`sessionSummary` return cookie instructions and destinations; the site applies
|
|
261
261
|
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
|
-
`READER_SSO_PROPERTY_KEYS` in its property sanitizer. `ReaderSsoBoot`
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
`READER_SSO_PROPERTY_KEYS` in its property sanitizer. `ReaderSsoBoot` is a
|
|
264
|
+
no-op compatibility export: a rendered page never starts Ledvery authorization
|
|
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
|
|
272
|
+
island that renders only a first name, in the reader's own time zone and locale,
|
|
273
|
+
after loading.
|
|
268
274
|
|
|
269
275
|
## Community pet sections
|
|
270
276
|
|
|
@@ -368,6 +368,11 @@ function getNetworkPublicationUrl(slug) {
|
|
|
368
368
|
if (!pub) return null;
|
|
369
369
|
return decorateNetworkUrl(pub.url + "/");
|
|
370
370
|
}
|
|
371
|
+
var readerHandoffReady = /* @__PURE__ */ new Set(["sanramontimes", "trivalleyweekly"]);
|
|
372
|
+
function getNetworkReaderHandoffUrl(slug) {
|
|
373
|
+
const publication = findPublicationBySlug(slug);
|
|
374
|
+
return publication && readerHandoffReady.has(slug) ? `${publication.url}/api/news/network-handoff` : null;
|
|
375
|
+
}
|
|
371
376
|
var NETWORK_HOSTNAMES = new Set(
|
|
372
377
|
localPublications.map((p) => {
|
|
373
378
|
try {
|
|
@@ -383,4 +388,4 @@ function isNetworkHostname(hostname) {
|
|
|
383
388
|
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
389
|
}
|
|
385
390
|
|
|
386
|
-
export { COOKIE_CATEGORIES, EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, recordConsentToCompliance, setupNetworkConsentDelegation, topicalPublications };
|
|
391
|
+
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 { hasConsented, rejectOptionalCookies, DEFAULT_CONSENT_HUB_URL, consentModel, isGpcActive, COOKIE_CONSENT_EVENT, acceptAllCookies, recordConsentToCompliance } from './chunk-
|
|
2
|
+
import { hasConsented, rejectOptionalCookies, DEFAULT_CONSENT_HUB_URL, consentModel, isGpcActive, COOKIE_CONSENT_EVENT, acceptAllCookies, recordConsentToCompliance } from './chunk-CKTTYAZY.js';
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { setupNetworkConsentDelegation, COOKIE_CONSENT_EVENT, listedPublications, decorateNetworkUrl } from './chunk-
|
|
2
|
+
import { setupNetworkConsentDelegation, COOKIE_CONSENT_EVENT, listedPublications, decorateNetworkUrl } from './chunk-CKTTYAZY.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 { effectivePreferences, getCookieConsent, COOKIE_CATEGORIES, setCookieConsent, recordConsentToCompliance, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED } from './chunk-
|
|
2
|
+
import { effectivePreferences, getCookieConsent, COOKIE_CATEGORIES, setCookieConsent, recordConsentToCompliance, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED } from './chunk-CKTTYAZY.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-
|
|
3
|
-
import './chunk-
|
|
2
|
+
export { CookieConsentBanner } from './chunk-EPAFIBBS.js';
|
|
3
|
+
import './chunk-CKTTYAZY.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { CookieSettingsView } from './chunk-
|
|
3
|
-
import './chunk-
|
|
2
|
+
export { CookieSettingsView } from './chunk-R6WTBTXO.js';
|
|
3
|
+
import './chunk-CKTTYAZY.js';
|
package/dist/index.cjs
CHANGED
|
@@ -1477,6 +1477,11 @@ function getNetworkPublicationUrl(slug) {
|
|
|
1477
1477
|
if (!pub) return null;
|
|
1478
1478
|
return compliance.decorateNetworkUrl(pub.url + "/");
|
|
1479
1479
|
}
|
|
1480
|
+
var readerHandoffReady = /* @__PURE__ */ new Set(["sanramontimes", "trivalleyweekly"]);
|
|
1481
|
+
function getNetworkReaderHandoffUrl(slug) {
|
|
1482
|
+
const publication = findPublicationBySlug(slug);
|
|
1483
|
+
return publication && readerHandoffReady.has(slug) ? `${publication.url}/api/news/network-handoff` : null;
|
|
1484
|
+
}
|
|
1480
1485
|
var NETWORK_HOSTNAMES = new Set(
|
|
1481
1486
|
localPublications.map((p) => {
|
|
1482
1487
|
try {
|
|
@@ -4001,6 +4006,7 @@ exports.findPublicationBySlug = findPublicationBySlug;
|
|
|
4001
4006
|
exports.formatArtworkCaption = formatArtworkCaption;
|
|
4002
4007
|
exports.getArticleCards = getArticleCards;
|
|
4003
4008
|
exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
|
|
4009
|
+
exports.getNetworkReaderHandoffUrl = getNetworkReaderHandoffUrl;
|
|
4004
4010
|
exports.getStoredTheme = getStoredTheme;
|
|
4005
4011
|
exports.getSystemTheme = getSystemTheme;
|
|
4006
4012
|
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-
|
|
2
|
-
export { NetworkCopyrightNotice } from './chunk-
|
|
1
|
+
export { LocalNetworkBar } from './chunk-F6HV2HRP.js';
|
|
2
|
+
export { NetworkCopyrightNotice } from './chunk-RSKB5NNE.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-
|
|
7
|
-
export { CookieSettingsView } from './chunk-
|
|
8
|
-
import { localPublications } from './chunk-
|
|
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-
|
|
6
|
+
export { CookieConsentBanner } from './chunk-EPAFIBBS.js';
|
|
7
|
+
export { CookieSettingsView } from './chunk-R6WTBTXO.js';
|
|
8
|
+
import { localPublications } from './chunk-CKTTYAZY.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-CKTTYAZY.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';
|
package/dist/network-bar.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { LocalNetworkBar } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { LocalNetworkBar } from './chunk-F6HV2HRP.js';
|
|
2
|
+
import './chunk-CKTTYAZY.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { NetworkCopyrightNotice } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { NetworkCopyrightNotice } from './chunk-RSKB5NNE.js';
|
|
2
|
+
import './chunk-CKTTYAZY.js';
|
package/dist/network.cjs
CHANGED
|
@@ -245,6 +245,11 @@ function getNetworkPublicationUrl(slug) {
|
|
|
245
245
|
if (!pub) return null;
|
|
246
246
|
return compliance.decorateNetworkUrl(pub.url + "/");
|
|
247
247
|
}
|
|
248
|
+
var readerHandoffReady = /* @__PURE__ */ new Set(["sanramontimes", "trivalleyweekly"]);
|
|
249
|
+
function getNetworkReaderHandoffUrl(slug) {
|
|
250
|
+
const publication = findPublicationBySlug(slug);
|
|
251
|
+
return publication && readerHandoffReady.has(slug) ? `${publication.url}/api/news/network-handoff` : null;
|
|
252
|
+
}
|
|
248
253
|
var NETWORK_HOSTNAMES = new Set(
|
|
249
254
|
localPublications.map((p) => {
|
|
250
255
|
try {
|
|
@@ -266,6 +271,7 @@ exports.communityPublications = communityPublications;
|
|
|
266
271
|
exports.findPublicationByHost = findPublicationByHost;
|
|
267
272
|
exports.findPublicationBySlug = findPublicationBySlug;
|
|
268
273
|
exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
|
|
274
|
+
exports.getNetworkReaderHandoffUrl = getNetworkReaderHandoffUrl;
|
|
269
275
|
exports.isNetworkHostname = isNetworkHostname;
|
|
270
276
|
exports.listedPublications = listedPublications;
|
|
271
277
|
exports.localPublications = localPublications;
|
package/dist/network.d.cts
CHANGED
|
@@ -58,7 +58,9 @@ 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
|
+
/** The fixed destination route; no identity or caller-selected return URL is carried. */
|
|
62
|
+
declare function getNetworkReaderHandoffUrl(slug: string): string | null;
|
|
61
63
|
/** Check whether a given hostname belongs to any publication in the ScaleMule news network. */
|
|
62
64
|
declare function isNetworkHostname(hostname: string): boolean;
|
|
63
65
|
|
|
64
|
-
export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
|
|
66
|
+
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,9 @@ 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
|
+
/** The fixed destination route; no identity or caller-selected return URL is carried. */
|
|
62
|
+
declare function getNetworkReaderHandoffUrl(slug: string): string | null;
|
|
61
63
|
/** Check whether a given hostname belongs to any publication in the ScaleMule news network. */
|
|
62
64
|
declare function isNetworkHostname(hostname: string): boolean;
|
|
63
65
|
|
|
64
|
-
export { EDITORIAL_PRINCIPLES, type EditorialPrinciple, NETWORK_PARENT, type NetworkParent, type PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications };
|
|
66
|
+
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-
|
|
1
|
+
export { EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, isNetworkHostname, listedPublications, localPublications, topicalPublications } from './chunk-CKTTYAZY.js';
|
|
@@ -4,46 +4,6 @@
|
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
7
|
-
// src/reader-sso-scheduler.ts
|
|
8
|
-
function mountReaderSsoScheduler({ document: doc, timers, eligible, attempt, idleDelayMs }) {
|
|
9
|
-
let active = true;
|
|
10
|
-
let attempted = false;
|
|
11
|
-
let idle;
|
|
12
|
-
let delay;
|
|
13
|
-
const cancelPending = () => {
|
|
14
|
-
if (idle !== void 0) timers.cancelIdleCallback?.(idle);
|
|
15
|
-
if (delay !== void 0) timers.clearTimeout(delay);
|
|
16
|
-
idle = delay = void 0;
|
|
17
|
-
};
|
|
18
|
-
const run = () => {
|
|
19
|
-
delay = void 0;
|
|
20
|
-
if (!active || attempted || !eligible()) return;
|
|
21
|
-
attempted = true;
|
|
22
|
-
attempt();
|
|
23
|
-
};
|
|
24
|
-
const defer = () => {
|
|
25
|
-
idle = void 0;
|
|
26
|
-
if (active && !attempted) delay = timers.setTimeout(run, idleDelayMs);
|
|
27
|
-
};
|
|
28
|
-
const schedule = () => {
|
|
29
|
-
if (!active || attempted) return;
|
|
30
|
-
cancelPending();
|
|
31
|
-
if (doc.readyState === "loading" || !eligible()) return;
|
|
32
|
-
if (timers.requestIdleCallback) idle = timers.requestIdleCallback(defer, { timeout: 4e3 });
|
|
33
|
-
else defer();
|
|
34
|
-
};
|
|
35
|
-
doc.addEventListener("DOMContentLoaded", schedule);
|
|
36
|
-
doc.addEventListener("visibilitychange", schedule);
|
|
37
|
-
doc.addEventListener("prerenderingchange", schedule);
|
|
38
|
-
schedule();
|
|
39
|
-
return () => {
|
|
40
|
-
active = false;
|
|
41
|
-
cancelPending();
|
|
42
|
-
doc.removeEventListener("DOMContentLoaded", schedule);
|
|
43
|
-
doc.removeEventListener("visibilitychange", schedule);
|
|
44
|
-
doc.removeEventListener("prerenderingchange", schedule);
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
7
|
var READER_SSO_BOT_PATTERN = /bot|crawl|spider|headless|lighthouse|pagespeed|preview|slurp|facebookexternalhit|embedly|python|curl|wget|phantom|puppeteer|playwright|selenium|prerender/i;
|
|
48
8
|
var READER_SSO_PRIVATE_PATHS = [
|
|
49
9
|
/^\/account(\/|$)/,
|
|
@@ -86,72 +46,7 @@ function isFormField(element) {
|
|
|
86
46
|
const tag = focused.tagName;
|
|
87
47
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || focused.isContentEditable === true;
|
|
88
48
|
}
|
|
89
|
-
|
|
90
|
-
function readAttempt() {
|
|
91
|
-
try {
|
|
92
|
-
const raw = window.sessionStorage.getItem(ATTEMPT_KEY);
|
|
93
|
-
const value = raw ? Number(raw) : NaN;
|
|
94
|
-
return Number.isFinite(value) ? value : null;
|
|
95
|
-
} catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function writeAttempt(now) {
|
|
100
|
-
try {
|
|
101
|
-
window.sessionStorage.setItem(ATTEMPT_KEY, String(now));
|
|
102
|
-
} catch {
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function browserEnvironment(flags) {
|
|
106
|
-
const doc = document;
|
|
107
|
-
let framed = false;
|
|
108
|
-
try {
|
|
109
|
-
framed = window.top !== window.self;
|
|
110
|
-
} catch {
|
|
111
|
-
framed = true;
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
...flags,
|
|
115
|
-
webdriver: navigator.webdriver === true,
|
|
116
|
-
userAgent: navigator.userAgent,
|
|
117
|
-
pathname: location.pathname,
|
|
118
|
-
search: location.search,
|
|
119
|
-
fieldFocused: isFormField(document.activeElement),
|
|
120
|
-
prerendering: doc.prerendering === true,
|
|
121
|
-
visibilityState: document.visibilityState,
|
|
122
|
-
framed,
|
|
123
|
-
lastAttemptAt: readAttempt(),
|
|
124
|
-
now: Date.now()
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function ReaderSsoBoot({ enabled = true, hasSession: hasSessionCookie, suppressed, startPath = "/api/news/ledvery-start", sessionPath = "/api/news/session", privatePaths, idleDelayMs = 1500 }) {
|
|
128
|
-
const [staleCookie, setStaleCookie] = react.useState(false);
|
|
129
|
-
react.useEffect(() => {
|
|
130
|
-
if (!enabled || !hasSessionCookie || staleCookie) return;
|
|
131
|
-
let cancelled = false;
|
|
132
|
-
fetch(sessionPath, { credentials: "same-origin", cache: "no-store" }).then((response) => response.json()).then((body) => {
|
|
133
|
-
if (!cancelled && (body?.authenticated !== true || body.session_upgrade_required === true)) setStaleCookie(true);
|
|
134
|
-
}).catch(() => {
|
|
135
|
-
});
|
|
136
|
-
return () => {
|
|
137
|
-
cancelled = true;
|
|
138
|
-
};
|
|
139
|
-
}, [enabled, hasSessionCookie, staleCookie, sessionPath]);
|
|
140
|
-
const hasSession = hasSessionCookie && !staleCookie;
|
|
141
|
-
react.useEffect(() => {
|
|
142
|
-
const flags = { enabled, hasSession, suppressed, privatePaths };
|
|
143
|
-
return mountReaderSsoScheduler({
|
|
144
|
-
document,
|
|
145
|
-
timers: window,
|
|
146
|
-
idleDelayMs,
|
|
147
|
-
eligible: () => silentSignInDecision(browserEnvironment(flags)).attempt === true,
|
|
148
|
-
attempt: () => {
|
|
149
|
-
const env = browserEnvironment(flags);
|
|
150
|
-
writeAttempt(env.now);
|
|
151
|
-
window.location.assign(silentSignInUrl(startPath, env.pathname, env.search));
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}, [enabled, hasSession, suppressed, startPath, privatePaths, idleDelayMs]);
|
|
49
|
+
function ReaderSsoBoot(_props) {
|
|
155
50
|
return null;
|
|
156
51
|
}
|
|
157
52
|
function firstName(fullName) {
|
|
@@ -28,16 +28,18 @@ type SilentSignInDecision = {
|
|
|
28
28
|
attempt: false;
|
|
29
29
|
reason: string;
|
|
30
30
|
};
|
|
31
|
+
/** @deprecated No automatic reader sign-in should use this decision. */
|
|
31
32
|
declare function silentSignInDecision(env: SilentSignInEnvironment): SilentSignInDecision;
|
|
33
|
+
/** @deprecated Use an explicit, user-initiated handoff flow instead. */
|
|
32
34
|
declare function silentSignInUrl(startPath: string, pathname: string, search: string): string;
|
|
33
35
|
declare function isFormField(element: Element | null): boolean;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Compatibility export for publications that still mount the old boot island.
|
|
38
|
+
* Reader identity is checked by the same-origin session UI; authentication
|
|
39
|
+
* must begin with an intentional click, never a timer or page-load navigation.
|
|
40
|
+
* @deprecated Remove this component from publication layouts.
|
|
39
41
|
*/
|
|
40
|
-
declare function ReaderSsoBoot(
|
|
42
|
+
declare function ReaderSsoBoot(_props: {
|
|
41
43
|
enabled?: boolean;
|
|
42
44
|
/** A reader session cookie is present (it may have been revoked elsewhere). */
|
|
43
45
|
hasSession: boolean;
|
|
@@ -28,16 +28,18 @@ type SilentSignInDecision = {
|
|
|
28
28
|
attempt: false;
|
|
29
29
|
reason: string;
|
|
30
30
|
};
|
|
31
|
+
/** @deprecated No automatic reader sign-in should use this decision. */
|
|
31
32
|
declare function silentSignInDecision(env: SilentSignInEnvironment): SilentSignInDecision;
|
|
33
|
+
/** @deprecated Use an explicit, user-initiated handoff flow instead. */
|
|
32
34
|
declare function silentSignInUrl(startPath: string, pathname: string, search: string): string;
|
|
33
35
|
declare function isFormField(element: Element | null): boolean;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Compatibility export for publications that still mount the old boot island.
|
|
38
|
+
* Reader identity is checked by the same-origin session UI; authentication
|
|
39
|
+
* must begin with an intentional click, never a timer or page-load navigation.
|
|
40
|
+
* @deprecated Remove this component from publication layouts.
|
|
39
41
|
*/
|
|
40
|
-
declare function ReaderSsoBoot(
|
|
42
|
+
declare function ReaderSsoBoot(_props: {
|
|
41
43
|
enabled?: boolean;
|
|
42
44
|
/** A reader session cookie is present (it may have been revoked elsewhere). */
|
|
43
45
|
hasSession: boolean;
|
|
@@ -2,46 +2,6 @@
|
|
|
2
2
|
import { useState, useEffect, useId } from 'react';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
// src/reader-sso-scheduler.ts
|
|
6
|
-
function mountReaderSsoScheduler({ document: doc, timers, eligible, attempt, idleDelayMs }) {
|
|
7
|
-
let active = true;
|
|
8
|
-
let attempted = false;
|
|
9
|
-
let idle;
|
|
10
|
-
let delay;
|
|
11
|
-
const cancelPending = () => {
|
|
12
|
-
if (idle !== void 0) timers.cancelIdleCallback?.(idle);
|
|
13
|
-
if (delay !== void 0) timers.clearTimeout(delay);
|
|
14
|
-
idle = delay = void 0;
|
|
15
|
-
};
|
|
16
|
-
const run = () => {
|
|
17
|
-
delay = void 0;
|
|
18
|
-
if (!active || attempted || !eligible()) return;
|
|
19
|
-
attempted = true;
|
|
20
|
-
attempt();
|
|
21
|
-
};
|
|
22
|
-
const defer = () => {
|
|
23
|
-
idle = void 0;
|
|
24
|
-
if (active && !attempted) delay = timers.setTimeout(run, idleDelayMs);
|
|
25
|
-
};
|
|
26
|
-
const schedule = () => {
|
|
27
|
-
if (!active || attempted) return;
|
|
28
|
-
cancelPending();
|
|
29
|
-
if (doc.readyState === "loading" || !eligible()) return;
|
|
30
|
-
if (timers.requestIdleCallback) idle = timers.requestIdleCallback(defer, { timeout: 4e3 });
|
|
31
|
-
else defer();
|
|
32
|
-
};
|
|
33
|
-
doc.addEventListener("DOMContentLoaded", schedule);
|
|
34
|
-
doc.addEventListener("visibilitychange", schedule);
|
|
35
|
-
doc.addEventListener("prerenderingchange", schedule);
|
|
36
|
-
schedule();
|
|
37
|
-
return () => {
|
|
38
|
-
active = false;
|
|
39
|
-
cancelPending();
|
|
40
|
-
doc.removeEventListener("DOMContentLoaded", schedule);
|
|
41
|
-
doc.removeEventListener("visibilitychange", schedule);
|
|
42
|
-
doc.removeEventListener("prerenderingchange", schedule);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
5
|
var READER_SSO_BOT_PATTERN = /bot|crawl|spider|headless|lighthouse|pagespeed|preview|slurp|facebookexternalhit|embedly|python|curl|wget|phantom|puppeteer|playwright|selenium|prerender/i;
|
|
46
6
|
var READER_SSO_PRIVATE_PATHS = [
|
|
47
7
|
/^\/account(\/|$)/,
|
|
@@ -84,72 +44,7 @@ function isFormField(element) {
|
|
|
84
44
|
const tag = focused.tagName;
|
|
85
45
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || focused.isContentEditable === true;
|
|
86
46
|
}
|
|
87
|
-
|
|
88
|
-
function readAttempt() {
|
|
89
|
-
try {
|
|
90
|
-
const raw = window.sessionStorage.getItem(ATTEMPT_KEY);
|
|
91
|
-
const value = raw ? Number(raw) : NaN;
|
|
92
|
-
return Number.isFinite(value) ? value : null;
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function writeAttempt(now) {
|
|
98
|
-
try {
|
|
99
|
-
window.sessionStorage.setItem(ATTEMPT_KEY, String(now));
|
|
100
|
-
} catch {
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function browserEnvironment(flags) {
|
|
104
|
-
const doc = document;
|
|
105
|
-
let framed = false;
|
|
106
|
-
try {
|
|
107
|
-
framed = window.top !== window.self;
|
|
108
|
-
} catch {
|
|
109
|
-
framed = true;
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
...flags,
|
|
113
|
-
webdriver: navigator.webdriver === true,
|
|
114
|
-
userAgent: navigator.userAgent,
|
|
115
|
-
pathname: location.pathname,
|
|
116
|
-
search: location.search,
|
|
117
|
-
fieldFocused: isFormField(document.activeElement),
|
|
118
|
-
prerendering: doc.prerendering === true,
|
|
119
|
-
visibilityState: document.visibilityState,
|
|
120
|
-
framed,
|
|
121
|
-
lastAttemptAt: readAttempt(),
|
|
122
|
-
now: Date.now()
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function ReaderSsoBoot({ enabled = true, hasSession: hasSessionCookie, suppressed, startPath = "/api/news/ledvery-start", sessionPath = "/api/news/session", privatePaths, idleDelayMs = 1500 }) {
|
|
126
|
-
const [staleCookie, setStaleCookie] = useState(false);
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (!enabled || !hasSessionCookie || staleCookie) return;
|
|
129
|
-
let cancelled = false;
|
|
130
|
-
fetch(sessionPath, { credentials: "same-origin", cache: "no-store" }).then((response) => response.json()).then((body) => {
|
|
131
|
-
if (!cancelled && (body?.authenticated !== true || body.session_upgrade_required === true)) setStaleCookie(true);
|
|
132
|
-
}).catch(() => {
|
|
133
|
-
});
|
|
134
|
-
return () => {
|
|
135
|
-
cancelled = true;
|
|
136
|
-
};
|
|
137
|
-
}, [enabled, hasSessionCookie, staleCookie, sessionPath]);
|
|
138
|
-
const hasSession = hasSessionCookie && !staleCookie;
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
const flags = { enabled, hasSession, suppressed, privatePaths };
|
|
141
|
-
return mountReaderSsoScheduler({
|
|
142
|
-
document,
|
|
143
|
-
timers: window,
|
|
144
|
-
idleDelayMs,
|
|
145
|
-
eligible: () => silentSignInDecision(browserEnvironment(flags)).attempt === true,
|
|
146
|
-
attempt: () => {
|
|
147
|
-
const env = browserEnvironment(flags);
|
|
148
|
-
writeAttempt(env.now);
|
|
149
|
-
window.location.assign(silentSignInUrl(startPath, env.pathname, env.search));
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}, [enabled, hasSession, suppressed, startPath, privatePaths, idleDelayMs]);
|
|
47
|
+
function ReaderSsoBoot(_props) {
|
|
153
48
|
return null;
|
|
154
49
|
}
|
|
155
50
|
function firstName(fullName) {
|
package/dist/reader-sso.cjs
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/reader-sso.d.cts
CHANGED
|
@@ -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 };
|
package/dist/reader-sso.d.ts
CHANGED
|
@@ -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 };
|
package/dist/reader-sso.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.89",
|
|
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": {
|