@scalemule/news-publication 0.0.70 → 0.0.72

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
@@ -246,6 +246,26 @@ Inventory is unavailable until a verified operational placement is active. `plac
246
246
 
247
247
  The consent banner now offers a choice in every geography where optional measurement requires one. `measurementConsent` is the server gate for current, explicitly allowed categories and GPC/DNT. Positive permission is local: unsigned URL parameters and a binary network hub cannot grant all categories. Rendering sponsorship recognition does not require tracking consent; measuring it requires both performance and advertising permission.
248
248
 
249
+ ## Reader sign-in through Ledvery
250
+
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,
253
+ explicit email sign-in, account linking, precedence between two accounts,
254
+ RP-initiated sign-out and server-recorded telemetry. A site supplies its cookie
255
+ prefix, canonical origin, Ledvery client id and a request adapter that performs
256
+ gateway calls with the site's own API key; the package never sees a credential
257
+ other than that key and the browser only ever sees host-only HttpOnly cookies.
258
+
259
+ `startSignIn`, `completeSignIn`, `linkAccount`, `switchAccount`, `signOut` and
260
+ `sessionSummary` return cookie instructions and destinations; the site applies
261
+ them with `applyCookies` and answers with private, no-store headers. Merge
262
+ `READER_SSO_EVENTS` into the site's analytics registry and allow
263
+ `READER_SSO_PROPERTY_KEYS` in its property sanitizer. `ReaderSsoBoot` performs at
264
+ most one silent attempt per page load and never on private paths, for bots,
265
+ while typing, in frames or in prerendered documents; the server's negative cache
266
+ makes a loop impossible. `ReaderGreeting` is a client island that renders only a
267
+ first name, in the reader's own time zone and locale, after loading.
268
+
249
269
  ## Community pet sections
250
270
 
251
271
  The `pets`, `pets-view`, `pets-forms`, `pets-interactions`, `pets-server`,
@@ -74,13 +74,14 @@ var PublicationArticleClient = class {
74
74
  { expected_generation: expectedGeneration }
75
75
  );
76
76
  }
77
- retryProcessing(articleId, expectedVersion, assetSourceFileId) {
77
+ retryProcessing(articleId, expectedVersion, assetSourceFileId, narrationProfileVersion) {
78
78
  return this.request(
79
79
  `/articles/${encodeURIComponent(articleId)}/processing`,
80
80
  "POST",
81
81
  {
82
82
  expected_version: expectedVersion,
83
- asset_source_file_id: assetSourceFileId
83
+ asset_source_file_id: assetSourceFileId,
84
+ narration_profile_version: narrationProfileVersion
84
85
  }
85
86
  );
86
87
  }
@@ -40,6 +40,9 @@ type ArticleProcessing = {
40
40
  narration: {
41
41
  status: string;
42
42
  attempts?: number | null;
43
+ audio_id?: string | null;
44
+ job_id?: string | null;
45
+ profile_override_version?: number | null;
43
46
  error_code?: string | null;
44
47
  last_progress_at?: string | null;
45
48
  };
@@ -59,6 +62,7 @@ type ArticleProcessing = {
59
62
  };
60
63
  type ArticleWorkflowOptions = {
61
64
  workflow_version: number;
65
+ profile_version: number;
62
66
  formats: ArticleFormat[];
63
67
  layouts: {
64
68
  definition: ArticleLayout;
@@ -136,7 +140,7 @@ declare class PublicationArticleClient {
136
140
  audio_id?: string | null;
137
141
  job_id?: string | null;
138
142
  }>;
139
- retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string): Promise<unknown>;
143
+ retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string, narrationProfileVersion?: number): Promise<unknown>;
140
144
  create(key: string, authorProfileId: string, document: ArticleDocument, reviewPolicy?: string): Promise<ArticleOperation>;
141
145
  save(key: string, articleId: string, generation: number, document: ArticleDocument, reason?: string): Promise<ArticleOperation>;
142
146
  command(articleId: string, generation: number, action: object): Promise<unknown>;
@@ -40,6 +40,9 @@ type ArticleProcessing = {
40
40
  narration: {
41
41
  status: string;
42
42
  attempts?: number | null;
43
+ audio_id?: string | null;
44
+ job_id?: string | null;
45
+ profile_override_version?: number | null;
43
46
  error_code?: string | null;
44
47
  last_progress_at?: string | null;
45
48
  };
@@ -59,6 +62,7 @@ type ArticleProcessing = {
59
62
  };
60
63
  type ArticleWorkflowOptions = {
61
64
  workflow_version: number;
65
+ profile_version: number;
62
66
  formats: ArticleFormat[];
63
67
  layouts: {
64
68
  definition: ArticleLayout;
@@ -136,7 +140,7 @@ declare class PublicationArticleClient {
136
140
  audio_id?: string | null;
137
141
  job_id?: string | null;
138
142
  }>;
139
- retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string): Promise<unknown>;
143
+ retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string, narrationProfileVersion?: number): Promise<unknown>;
140
144
  create(key: string, authorProfileId: string, document: ArticleDocument, reviewPolicy?: string): Promise<ArticleOperation>;
141
145
  save(key: string, articleId: string, generation: number, document: ArticleDocument, reason?: string): Promise<ArticleOperation>;
142
146
  command(articleId: string, generation: number, action: object): Promise<unknown>;
@@ -72,13 +72,14 @@ var PublicationArticleClient = class {
72
72
  { expected_generation: expectedGeneration }
73
73
  );
74
74
  }
75
- retryProcessing(articleId, expectedVersion, assetSourceFileId) {
75
+ retryProcessing(articleId, expectedVersion, assetSourceFileId, narrationProfileVersion) {
76
76
  return this.request(
77
77
  `/articles/${encodeURIComponent(articleId)}/processing`,
78
78
  "POST",
79
79
  {
80
80
  expected_version: expectedVersion,
81
- asset_source_file_id: assetSourceFileId
81
+ asset_source_file_id: assetSourceFileId,
82
+ narration_profile_version: narrationProfileVersion
82
83
  }
83
84
  );
84
85
  }
@@ -0,0 +1,12 @@
1
+ // src/reader-sso-events.ts
2
+ var READER_SSO_EVENTS = {
3
+ reader_signin_started: { category: "engagement", source: "server" },
4
+ reader_signin_completed: { category: "conversion", source: "server" },
5
+ reader_signin_failed: { category: "system", source: "server" },
6
+ reader_sso_check: { category: "system", source: "server" },
7
+ reader_signed_out: { category: "engagement", source: "server" },
8
+ reader_account_linked: { category: "conversion", source: "server" }
9
+ };
10
+ var READER_SSO_PROPERTY_KEYS = ["method", "mode", "is_new", "reason", "outcome", "transaction"];
11
+
12
+ export { READER_SSO_EVENTS, READER_SSO_PROPERTY_KEYS };
package/dist/index.cjs CHANGED
@@ -3586,6 +3586,17 @@ function campaignReportHtml(report) {
3586
3586
  <footer><p>${escape(report.reporting_note)}</p><p>${escape(report.editorial_independence)}</p><p>Campaign reference: ${escape(report.campaign_id)}</p></footer></body></html>`;
3587
3587
  }
3588
3588
 
3589
+ // src/reader-sso-events.ts
3590
+ var READER_SSO_EVENTS = {
3591
+ reader_signin_started: { category: "engagement", source: "server" },
3592
+ reader_signin_completed: { category: "conversion", source: "server" },
3593
+ reader_signin_failed: { category: "system", source: "server" },
3594
+ reader_sso_check: { category: "system", source: "server" },
3595
+ reader_signed_out: { category: "engagement", source: "server" },
3596
+ reader_account_linked: { category: "conversion", source: "server" }
3597
+ };
3598
+ var READER_SSO_PROPERTY_KEYS = ["method", "mode", "is_new", "reason", "outcome", "transaction"];
3599
+
3589
3600
  // src/article-presentation.ts
3590
3601
  var standard = {
3591
3602
  id: "standard",
@@ -3742,6 +3753,8 @@ exports.NETWORK_PARENT = NETWORK_PARENT;
3742
3753
  exports.NEWS_CACHE = NEWS_CACHE;
3743
3754
  exports.NetworkCopyrightNotice = NetworkCopyrightNotice;
3744
3755
  exports.NewsError = NewsError;
3756
+ exports.READER_SSO_EVENTS = READER_SSO_EVENTS;
3757
+ exports.READER_SSO_PROPERTY_KEYS = READER_SSO_PROPERTY_KEYS;
3745
3758
  exports.SESSION_IDLE_MS = SESSION_IDLE_MS;
3746
3759
  exports.SPONSORSHIP_OFFERINGS = SPONSORSHIP_OFFERINGS;
3747
3760
  exports.SPONSORSHIP_POLICY = SPONSORSHIP_POLICY;
package/dist/index.d.cts CHANGED
@@ -17,6 +17,7 @@ export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './commun
17
17
  export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.cjs';
18
18
  import { M as Measurement } from './sponsorships-CtnBe3S8.cjs';
19
19
  export { a as Campaign, b as CampaignStatus, c as CampaignTarget, C as Creative, d as Placement, e as PlacementType, P as PublicAcknowledgment, S as SPONSORSHIP_OFFERINGS, f as SPONSORSHIP_POLICY, g as SponsorParty, p as placementAvailable, s as safeSponsorUrl } from './sponsorships-CtnBe3S8.cjs';
20
+ export { R as READER_SSO_EVENTS, a as READER_SSO_PROPERTY_KEYS, b as ReaderSsoEventDefinition, c as ReaderSsoEventName } from './reader-sso-events-DBw8pGSM.cjs';
20
21
  export { A as ArticleFormat, a as ArticleLayout, b as ArticlePresentation, c as articleFormatLabel, d as articleHeroRatio, e as articlePresentation } from './article-presentation-BrNCcF-5.cjs';
21
22
  import './pets.cjs';
22
23
 
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './commun
17
17
  export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.js';
18
18
  import { M as Measurement } from './sponsorships-CtnBe3S8.js';
19
19
  export { a as Campaign, b as CampaignStatus, c as CampaignTarget, C as Creative, d as Placement, e as PlacementType, P as PublicAcknowledgment, S as SPONSORSHIP_OFFERINGS, f as SPONSORSHIP_POLICY, g as SponsorParty, p as placementAvailable, s as safeSponsorUrl } from './sponsorships-CtnBe3S8.js';
20
+ export { R as READER_SSO_EVENTS, a as READER_SSO_PROPERTY_KEYS, b as ReaderSsoEventDefinition, c as ReaderSsoEventName } from './reader-sso-events-DBw8pGSM.js';
20
21
  export { A as ArticleFormat, a as ArticleLayout, b as ArticlePresentation, c as articleFormatLabel, d as articleHeroRatio, e as articlePresentation } from './article-presentation-BrNCcF-5.js';
21
22
  import './pets.js';
22
23
 
package/dist/index.js CHANGED
@@ -1,17 +1,18 @@
1
+ export { LocalNetworkBar } from './chunk-Q77T2S7B.js';
1
2
  export { NetworkCopyrightNotice } from './chunk-FWEYKWKP.js';
2
3
  export { CURATED_COMMUNITY_CARDS, VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-KYUVXEJY.js';
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';
3
5
  export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-AMLVCZDP.js';
4
6
  export { CookieConsentBanner } from './chunk-XSAVJJVW.js';
5
7
  export { CookieSettingsView } from './chunk-X7AU7DV6.js';
6
- 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';
7
- export { LocalNetworkBar } from './chunk-Q77T2S7B.js';
8
8
  import { localPublications } from './chunk-XWXPERLK.js';
9
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';
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';
10
11
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
11
12
  export { articleFormatLabel, articleHeroRatio, articlePresentation, formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-DN3TBZLK.js';
12
13
  import { definePublication } from './chunk-57YVL6BD.js';
13
14
  export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-57YVL6BD.js';
14
- 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';
15
+ export { READER_SSO_EVENTS, READER_SSO_PROPERTY_KEYS } from './chunk-CVD64KP6.js';
15
16
  export { SPONSORSHIP_OFFERINGS, SPONSORSHIP_POLICY, placementAvailable, safeSponsorUrl } from './chunk-66GI2IX6.js';
16
17
  import './chunk-AFJ6S5WJ.js';
17
18
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -0,0 +1,364 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
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;
8
+ var READER_SSO_PRIVATE_PATHS = [
9
+ /^\/account(\/|$)/,
10
+ /^\/api(\/|$)/,
11
+ /^\/review(\/|$)/,
12
+ /^\/reset-password(\/|$)/,
13
+ /^\/verify-email(\/|$)/,
14
+ /^\/claim(\/|$)/,
15
+ /^\/tip(\/|$)/,
16
+ /^\/news\/unsubscribe(\/|$)/,
17
+ /^\/pets\/account(\/|$)/,
18
+ /^\/newsletter(\/|$)/
19
+ ];
20
+ var FLOW_QUERY = ["signin", "signed_out", "switch", "link", "subscription", "code", "state", "error"];
21
+ var READER_SSO_ATTEMPT_WINDOW_MS = 5 * 6e4;
22
+ function silentSignInDecision(env) {
23
+ if (!env.enabled) return { attempt: false, reason: "disabled" };
24
+ if (env.hasSession) return { attempt: false, reason: "session" };
25
+ if (env.suppressed) return { attempt: false, reason: "suppressed" };
26
+ if (env.webdriver) return { attempt: false, reason: "webdriver" };
27
+ if (!env.userAgent || READER_SSO_BOT_PATTERN.test(env.userAgent)) return { attempt: false, reason: "bot" };
28
+ if (env.framed) return { attempt: false, reason: "framed" };
29
+ if (env.prerendering || env.visibilityState !== "visible") return { attempt: false, reason: "hidden" };
30
+ if ((env.privatePaths ?? READER_SSO_PRIVATE_PATHS).some((pattern) => pattern.test(env.pathname))) return { attempt: false, reason: "private" };
31
+ const params = new URLSearchParams(env.search);
32
+ if (FLOW_QUERY.some((key) => params.has(key))) return { attempt: false, reason: "flow" };
33
+ if (env.fieldFocused) return { attempt: false, reason: "typing" };
34
+ if (env.lastAttemptAt !== null && env.now - env.lastAttemptAt < READER_SSO_ATTEMPT_WINDOW_MS) return { attempt: false, reason: "recent" };
35
+ return { attempt: true };
36
+ }
37
+ function silentSignInUrl(startPath, pathname, search) {
38
+ return `${startPath}?${new URLSearchParams({ mode: "silent", next: pathname + search })}`;
39
+ }
40
+ function isFormField(element) {
41
+ if (!element) return false;
42
+ const tag = element.tagName;
43
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element.isContentEditable === true;
44
+ }
45
+ var ATTEMPT_KEY = "reader-sso-attempt-v1";
46
+ function readAttempt() {
47
+ try {
48
+ const raw = window.sessionStorage.getItem(ATTEMPT_KEY);
49
+ const value = raw ? Number(raw) : NaN;
50
+ return Number.isFinite(value) ? value : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function writeAttempt(now) {
56
+ try {
57
+ window.sessionStorage.setItem(ATTEMPT_KEY, String(now));
58
+ } catch {
59
+ }
60
+ }
61
+ function browserEnvironment(flags) {
62
+ const doc = document;
63
+ let framed = false;
64
+ try {
65
+ framed = window.top !== window.self;
66
+ } catch {
67
+ framed = true;
68
+ }
69
+ return {
70
+ ...flags,
71
+ webdriver: navigator.webdriver === true,
72
+ userAgent: navigator.userAgent,
73
+ pathname: location.pathname,
74
+ search: location.search,
75
+ fieldFocused: isFormField(document.activeElement),
76
+ prerendering: doc.prerendering === true,
77
+ visibilityState: document.visibilityState,
78
+ framed,
79
+ lastAttemptAt: readAttempt(),
80
+ now: Date.now()
81
+ };
82
+ }
83
+ function ReaderSsoBoot({ enabled = true, hasSession: hasSessionCookie, suppressed, startPath = "/api/news/ledvery-start", sessionPath = "/api/news/session", privatePaths, idleDelayMs = 1500 }) {
84
+ const [staleCookie, setStaleCookie] = react.useState(false);
85
+ react.useEffect(() => {
86
+ if (!enabled || !hasSessionCookie || staleCookie) return;
87
+ let cancelled = false;
88
+ fetch(sessionPath, { credentials: "same-origin", cache: "no-store" }).then((response) => response.json()).then((body) => {
89
+ if (!cancelled && body?.authenticated !== true) setStaleCookie(true);
90
+ }).catch(() => {
91
+ });
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [enabled, hasSessionCookie, staleCookie, sessionPath]);
96
+ const hasSession = hasSessionCookie && !staleCookie;
97
+ react.useEffect(() => {
98
+ if (silentSignInDecision(browserEnvironment({ enabled, hasSession, suppressed, privatePaths })).attempt !== true) return;
99
+ let cancelled = false;
100
+ let idle;
101
+ const go = () => {
102
+ if (cancelled) return;
103
+ const env = browserEnvironment({ enabled, hasSession, suppressed, privatePaths });
104
+ if (silentSignInDecision(env).attempt !== true) return;
105
+ writeAttempt(env.now);
106
+ window.location.assign(silentSignInUrl(startPath, env.pathname, env.search));
107
+ };
108
+ const whenIdle = () => {
109
+ const w = window;
110
+ if (typeof w.requestIdleCallback === "function") idle = w.requestIdleCallback(() => {
111
+ idle = window.setTimeout(go, idleDelayMs);
112
+ }, { timeout: 4e3 });
113
+ else idle = window.setTimeout(go, idleDelayMs);
114
+ };
115
+ if (document.readyState === "complete") whenIdle();
116
+ else window.addEventListener("load", whenIdle, { once: true });
117
+ return () => {
118
+ cancelled = true;
119
+ window.removeEventListener("load", whenIdle);
120
+ if (idle !== void 0) window.clearTimeout(idle);
121
+ };
122
+ }, [enabled, hasSession, suppressed, startPath, privatePaths, idleDelayMs]);
123
+ return null;
124
+ }
125
+ function firstName(fullName) {
126
+ if (typeof fullName !== "string") return "";
127
+ const token = fullName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().split(/\s+/u)[0] ?? "";
128
+ return Array.from(token).slice(0, 40).join("");
129
+ }
130
+ function localHour(now, timeZone, locale) {
131
+ const options = { hour: "numeric", hourCycle: "h23" };
132
+ const attempts = [[locale ?? void 0, timeZone ?? void 0], [void 0, timeZone ?? void 0], [void 0, void 0]];
133
+ for (const [loc, tz] of attempts) {
134
+ try {
135
+ const part = new Intl.DateTimeFormat(loc, { ...options, ...tz ? { timeZone: tz } : {} }).formatToParts(now).find((p) => p.type === "hour");
136
+ const hour = part ? Number(part.value) : NaN;
137
+ if (Number.isInteger(hour) && hour >= 0 && hour < 24) return hour;
138
+ } catch {
139
+ }
140
+ }
141
+ return now.getHours();
142
+ }
143
+ function greetingWord(hour) {
144
+ if (hour >= 5 && hour < 12) return "Good morning";
145
+ if (hour >= 12 && hour < 18) return "Good afternoon";
146
+ return "Good evening";
147
+ }
148
+ function greetingText(profile, now = /* @__PURE__ */ new Date(), fallback = "Welcome back") {
149
+ const name = firstName(profile.full_name);
150
+ if (!name) return fallback;
151
+ return `${greetingWord(localHour(now, profile.time_zone, profile.locale))}, ${name}`;
152
+ }
153
+ function ReaderGreeting({ endpoint = "/api/news/session", className, fallback = "Welcome back", as: Tag = "span" }) {
154
+ const [text, setText] = react.useState(null);
155
+ react.useEffect(() => {
156
+ let alive = true;
157
+ const controller = new AbortController();
158
+ fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: controller.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
159
+ if (!alive || !summary || summary.authenticated !== true) return;
160
+ setText(greetingText(summary, /* @__PURE__ */ new Date(), fallback));
161
+ }).catch(() => {
162
+ });
163
+ return () => {
164
+ alive = false;
165
+ controller.abort();
166
+ };
167
+ }, [endpoint, fallback]);
168
+ if (text === null) return null;
169
+ return /* @__PURE__ */ jsxRuntime.jsx(Tag, { className, "data-reader-greeting": "", children: text });
170
+ }
171
+ async function postJson(endpoint, body, timeoutMs = 3e4) {
172
+ const response = await fetch(endpoint, {
173
+ method: "POST",
174
+ headers: { "content-type": "application/json" },
175
+ body: JSON.stringify(body),
176
+ credentials: "same-origin",
177
+ signal: AbortSignal.timeout(timeoutMs)
178
+ });
179
+ const data = await response.json().catch(() => ({}));
180
+ if (!response.ok) throw Object.assign(new Error(data.error || "Please try again."), { mfaRequired: data.mfa_required === true });
181
+ return data;
182
+ }
183
+ function GoogleMark() {
184
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
185
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21.805 10.023H12.24v3.955h5.483c-.237 1.273-.958 2.352-2.042 3.073v2.552h3.305c1.936-1.782 3.05-4.41 3.05-7.603 0-.676-.06-1.326-.171-1.977Z", fill: "#4285F4" }),
186
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.24 22c2.745 0 5.047-.909 6.73-2.447l-3.305-2.552c-.918.615-2.094.978-3.425.978-2.636 0-4.87-1.78-5.67-4.173H3.154v2.632A10.16 10.16 0 0 0 12.24 22Z", fill: "#34A853" }),
187
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.57 13.806A6.112 6.112 0 0 1 6.253 12c0-.627.109-1.235.317-1.806V7.562H3.154A10.157 10.157 0 0 0 2 12c0 1.633.39 3.177 1.154 4.438l3.416-2.632Z", fill: "#FBBC04" }),
188
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.24 6.02c1.493 0 2.833.514 3.888 1.523l2.913-2.913C17.281 2.998 14.979 2 12.24 2A10.16 10.16 0 0 0 3.154 7.562l3.416 2.632c.8-2.393 3.034-4.174 5.67-4.174Z", fill: "#EA4335" })
189
+ ] });
190
+ }
191
+ function MailMark() {
192
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
193
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
194
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m3 7 9 6 9-6" })
195
+ ] });
196
+ }
197
+ var LEDVERY_BUTTON_LABELS = {
198
+ google: { label: "Continue with Google", busy: "Opening Google\u2026" },
199
+ email: { label: "Continue with email", busy: "Opening sign-in\u2026" }
200
+ };
201
+ function LedveryButton({
202
+ provider = "google",
203
+ subscribe = false,
204
+ disabled = false,
205
+ returnTo = "",
206
+ startPath = "/api/news/ledvery-start",
207
+ label,
208
+ busyLabel,
209
+ description,
210
+ classNames = {},
211
+ onStart
212
+ }) {
213
+ const [busy, setBusy] = react.useState(false);
214
+ const [error, setError] = react.useState("");
215
+ const descriptionId = react.useId();
216
+ const labels = LEDVERY_BUTTON_LABELS[provider];
217
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames.wrapper, "data-reader-signin": provider, children: [
218
+ /* @__PURE__ */ jsxRuntime.jsxs(
219
+ "button",
220
+ {
221
+ type: "button",
222
+ className: classNames.button,
223
+ disabled: busy || disabled,
224
+ "aria-describedby": description ? descriptionId : void 0,
225
+ onClick: async () => {
226
+ setBusy(true);
227
+ setError("");
228
+ try {
229
+ onStart?.(provider);
230
+ } catch {
231
+ }
232
+ try {
233
+ const data = await postJson(startPath, { provider, subscribe, consent: subscribe, returnTo });
234
+ if (!data.url) throw new Error("Sign-in is temporarily unavailable. Please try again.");
235
+ window.location.assign(data.url);
236
+ } catch (e) {
237
+ setError(e instanceof Error ? e.message : "Please try again.");
238
+ setBusy(false);
239
+ }
240
+ },
241
+ children: [
242
+ provider === "google" ? /* @__PURE__ */ jsxRuntime.jsx(GoogleMark, {}) : /* @__PURE__ */ jsxRuntime.jsx(MailMark, {}),
243
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: busy ? busyLabel ?? labels.busy : label ?? labels.label })
244
+ ]
245
+ }
246
+ ),
247
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descriptionId, className: classNames.fine, children: description }),
248
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: classNames.error, children: error })
249
+ ] });
250
+ }
251
+ function AccountLinkForm({
252
+ challengeId,
253
+ hint = "",
254
+ endpoint = "/api/news/ledvery-link",
255
+ next = "",
256
+ subscribe = false,
257
+ classNames = {},
258
+ onLinked,
259
+ onCancel
260
+ }) {
261
+ const [busy, setBusy] = react.useState(false);
262
+ const [error, setError] = react.useState("");
263
+ const [password, setPassword] = react.useState("");
264
+ const [mfaRequired, setMfaRequired] = react.useState(false);
265
+ const [code, setCode] = react.useState("");
266
+ return /* @__PURE__ */ jsxRuntime.jsxs(
267
+ "form",
268
+ {
269
+ className: classNames.form,
270
+ "data-reader-link": "",
271
+ onSubmit: (event) => {
272
+ event.preventDefault();
273
+ setBusy(true);
274
+ setError("");
275
+ postJson(endpoint, { challenge_id: challengeId, password, next, subscribe, ...mfaRequired && code ? { mfa_code: code } : {} }).then((result) => {
276
+ setPassword("");
277
+ setCode("");
278
+ if (onLinked) onLinked(result);
279
+ else window.location.assign(result.location || "/account");
280
+ }).catch((e) => {
281
+ if (e?.mfaRequired) setMfaRequired(true);
282
+ setCode("");
283
+ setError(e instanceof Error ? e.message : "Please try again.");
284
+ setBusy(false);
285
+ });
286
+ },
287
+ children: [
288
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "You already have a reader account" }),
289
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: classNames.fine, children: [
290
+ hint ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
291
+ "An account for ",
292
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: hint }),
293
+ " already exists. "
294
+ ] }) : null,
295
+ "Enter its password once to connect your sign-in. Your preferences and subscriptions stay as they are."
296
+ ] }),
297
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
298
+ "Reader account password",
299
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "password", autoComplete: "current-password", required: true, minLength: 1, maxLength: 1024, value: password, onChange: (e) => setPassword(e.target.value) })
300
+ ] }),
301
+ mfaRequired && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
302
+ "Authentication code",
303
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", inputMode: "numeric", autoComplete: "one-time-code", required: true, maxLength: 32, value: code, onChange: (e) => setCode(e.target.value) })
304
+ ] }),
305
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: classNames.button, disabled: busy || !password || mfaRequired && !code, children: busy ? "Connecting\u2026" : "Connect and sign in" }),
306
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: classNames.secondary, disabled: busy, onClick: onCancel, children: "Start over" }),
307
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: classNames.error, children: error })
308
+ ]
309
+ }
310
+ );
311
+ }
312
+ function AccountSwitchPrompt({
313
+ currentName,
314
+ pendingName,
315
+ endpoint = "/api/news/switch-account",
316
+ classNames = {},
317
+ onDone
318
+ }) {
319
+ const [busy, setBusy] = react.useState(null);
320
+ const [error, setError] = react.useState("");
321
+ const current = firstName(currentName) || "your current account";
322
+ const pending = firstName(pendingName) || "the new account";
323
+ const choose = (choice) => {
324
+ setBusy(choice);
325
+ setError("");
326
+ postJson(endpoint, { choice }).then((result) => {
327
+ if (onDone) onDone({ ...result, choice });
328
+ else window.location.assign(result.location || "/account");
329
+ }).catch((e) => {
330
+ setError(e instanceof Error ? e.message : "Please try again.");
331
+ setBusy(null);
332
+ });
333
+ };
334
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames.wrapper, "data-reader-switch": "", role: "group", "aria-label": "Choose an account", children: [
335
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Which account would you like to use?" }),
336
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: classNames.fine, children: [
337
+ "You are signed in as ",
338
+ current,
339
+ ". A different account, ",
340
+ pending,
341
+ ", just signed in through Ledvery."
342
+ ] }),
343
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: classNames.button, disabled: busy !== null, onClick: () => choose("continue"), children: busy === "continue" ? "Switching\u2026" : `Continue as ${pending}` }),
344
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: classNames.secondary, disabled: busy !== null, onClick: () => choose("stay"), children: busy === "stay" ? "One moment\u2026" : `Stay as ${current}` }),
345
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: classNames.error, children: error })
346
+ ] });
347
+ }
348
+
349
+ exports.AccountLinkForm = AccountLinkForm;
350
+ exports.AccountSwitchPrompt = AccountSwitchPrompt;
351
+ exports.LEDVERY_BUTTON_LABELS = LEDVERY_BUTTON_LABELS;
352
+ exports.LedveryButton = LedveryButton;
353
+ exports.READER_SSO_ATTEMPT_WINDOW_MS = READER_SSO_ATTEMPT_WINDOW_MS;
354
+ exports.READER_SSO_BOT_PATTERN = READER_SSO_BOT_PATTERN;
355
+ exports.READER_SSO_PRIVATE_PATHS = READER_SSO_PRIVATE_PATHS;
356
+ exports.ReaderGreeting = ReaderGreeting;
357
+ exports.ReaderSsoBoot = ReaderSsoBoot;
358
+ exports.firstName = firstName;
359
+ exports.greetingText = greetingText;
360
+ exports.greetingWord = greetingWord;
361
+ exports.isFormField = isFormField;
362
+ exports.localHour = localHour;
363
+ exports.silentSignInDecision = silentSignInDecision;
364
+ exports.silentSignInUrl = silentSignInUrl;