@scalemule/news-publication 0.0.70 → 0.0.73

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.
@@ -0,0 +1,506 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // src/reader-sso.ts
6
+
7
+ // src/reader-sso-events.ts
8
+ var READER_SSO_EVENTS = {
9
+ reader_signin_started: { category: "engagement", source: "server" },
10
+ reader_signin_completed: { category: "conversion", source: "server" },
11
+ reader_signin_failed: { category: "system", source: "server" },
12
+ reader_sso_check: { category: "system", source: "server" },
13
+ reader_signed_out: { category: "engagement", source: "server" },
14
+ reader_account_linked: { category: "conversion", source: "server" }
15
+ };
16
+ var READER_SSO_PROPERTY_KEYS = ["method", "mode", "is_new", "reason", "outcome", "transaction"];
17
+
18
+ // src/reader-sso.ts
19
+ var READER_SSO_FLOW_SECONDS = 600;
20
+ var READER_SSO_NEGATIVE_CACHE_SECONDS = 300;
21
+ var READER_SSO_SIGNED_OUT_SECONDS = 86400;
22
+ var READER_SSO_PENDING_SECONDS = 600;
23
+ var LEDVERY_ISSUER_ORIGIN = "https://api.ledvery.com";
24
+ var SILENT_NEGATIVE_ERRORS = ["login_required", "consent_required", "interaction_required", "account_selection_required"];
25
+ var COOKIE_PREFIX = /^__Host-[A-Za-z0-9_-]{1,60}$/;
26
+ var SESSION_TOKEN = /^[A-Za-z0-9._~-]{1,2048}$/;
27
+ var CHALLENGE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
28
+ var HEX64 = /^[a-f0-9]{64}$/;
29
+ function readerSsoCookieNames(prefix) {
30
+ if (!COOKIE_PREFIX.test(prefix)) throw new Error("Reader cookie prefix must be a short __Host- name");
31
+ return {
32
+ session: `${prefix}-reader`,
33
+ flow: `${prefix}-ledvery`,
34
+ newsletter: `${prefix}-newsletter`,
35
+ ssoChecked: `${prefix}-sso-checked`,
36
+ signedOut: `${prefix}-signed-out`,
37
+ pending: `${prefix}-sso-pending`
38
+ };
39
+ }
40
+ var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
41
+ var clear = (name) => ({ name, value: "", maxAge: 0 });
42
+ function applyCookies(jar, cookies) {
43
+ for (const cookie of cookies) {
44
+ jar.set(cookie.name, cookie.value, {
45
+ ...readerSsoCookieOptions,
46
+ ...cookie.maxAge !== void 0 ? { maxAge: cookie.maxAge } : {},
47
+ ...cookie.expires ? { expires: cookie.expires } : {}
48
+ });
49
+ }
50
+ }
51
+ function setCookieHeader(cookie) {
52
+ const parts = [`${cookie.name}=${cookie.value}`, "Path=/", "HttpOnly", "Secure", "SameSite=Lax"];
53
+ if (cookie.maxAge !== void 0) parts.push(`Max-Age=${cookie.maxAge}`);
54
+ if (cookie.expires) parts.push(`Expires=${cookie.expires.toUTCString()}`);
55
+ return parts.join("; ");
56
+ }
57
+ function cookieReader(header) {
58
+ const parts = (header || "").split(";").map((part) => part.trim());
59
+ return (name) => {
60
+ const values = parts.filter((part) => part.startsWith(name + "="));
61
+ return values.length === 1 ? values[0].slice(name.length + 1) : void 0;
62
+ };
63
+ }
64
+ function payload(response) {
65
+ const body = response.body;
66
+ const data = body && typeof body === "object" ? body.data : void 0;
67
+ return data && typeof data === "object" ? data : body && typeof body === "object" ? body : {};
68
+ }
69
+ function errorCode(response) {
70
+ const body = response.body;
71
+ const error = body && typeof body === "object" ? body.error : void 0;
72
+ const code = error && typeof error === "object" ? error.code : body && typeof body === "object" ? body.code : void 0;
73
+ return typeof code === "string" ? code : "";
74
+ }
75
+ function failed(response) {
76
+ const body = response.body;
77
+ return response.status < 200 || response.status >= 300 || Boolean(body && typeof body === "object" && body.success === false);
78
+ }
79
+ function localReturnPath(value) {
80
+ if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.length > 500) return "";
81
+ if (/[\\\r\n\u0000-\u001f]/.test(value) || value === "/api" || value.startsWith("/api/")) return "";
82
+ return value;
83
+ }
84
+ function resolve(config) {
85
+ const origin = new URL(config.origin);
86
+ if (origin.protocol !== "https:" || origin.origin !== config.origin) throw new Error("Reader SSO origin must be a bare https origin");
87
+ if (!config.key) throw new Error("Reader SSO key is required");
88
+ const accountPath = localReturnPath(config.accountPath ?? "/account") || "/account";
89
+ const returnPath = config.returnPath ?? ((next) => localReturnPath(next));
90
+ return {
91
+ ...config,
92
+ names: readerSsoCookieNames(config.prefix),
93
+ callbackPath: localReturnPath(config.callbackPath ?? "/api/news/ledvery-callback") || "/api/news/ledvery-callback",
94
+ accountPath,
95
+ issuerOrigin: new URL(config.issuerOrigin ?? LEDVERY_ISSUER_ORIGIN).origin,
96
+ returnPath,
97
+ retryPath: config.retryPath ?? ((flow) => flow?.returnTo ? `${accountPath}?signin=retry&next=${encodeURIComponent(flow.returnTo)}` : `${accountPath}?signin=retry`),
98
+ now: config.now ?? (() => Date.now()),
99
+ random: config.random ?? (() => crypto.randomBytes(16).toString("hex"))
100
+ };
101
+ }
102
+ function emit(config, event) {
103
+ try {
104
+ config.record?.(event);
105
+ } catch {
106
+ }
107
+ }
108
+ function stateDigest(state) {
109
+ return crypto.createHash("sha256").update(state).digest("hex");
110
+ }
111
+ function validState(state, digest) {
112
+ if (!state || state.length > 2048 || !digest || !HEX64.test(digest)) return false;
113
+ return crypto.timingSafeEqual(Buffer.from(stateDigest(state), "hex"), Buffer.from(digest, "hex"));
114
+ }
115
+ function seal(label, body, key) {
116
+ const encoded = Buffer.from(JSON.stringify(body)).toString("base64url");
117
+ return encoded + "." + crypto.createHmac("sha256", key).update(`${label}:` + encoded).digest("hex");
118
+ }
119
+ function open(label, cookie, key) {
120
+ if (!key || !cookie || cookie.length > 4096) return null;
121
+ const [encoded, signature, extra] = cookie.split(".");
122
+ if (extra !== void 0 || !encoded || !signature || !HEX64.test(signature)) return null;
123
+ const expected = crypto.createHmac("sha256", key).update(`${label}:` + encoded).digest();
124
+ if (!crypto.timingSafeEqual(expected, Buffer.from(signature, "hex"))) return null;
125
+ try {
126
+ const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
127
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function sealReaderFlow(input, key, now = Date.now()) {
133
+ const body = {
134
+ digest: stateDigest(input.state),
135
+ subscribe: input.subscribe === true,
136
+ expires: now + READER_SSO_FLOW_SECONDS * 1e3,
137
+ returnTo: localReturnPath(input.returnTo ?? "")
138
+ };
139
+ if (input.mode) body.mode = input.mode;
140
+ if (input.provider) body.provider = input.provider;
141
+ if (input.tx) body.tx = input.tx;
142
+ return seal("reader-flow:v1", body, key);
143
+ }
144
+ function openReaderFlow(cookie, key, now = Date.now(), returnPath = (next) => localReturnPath(next)) {
145
+ const raw = open("reader-flow:v1", cookie, key);
146
+ if (!raw) return null;
147
+ const mode = raw.mode === "silent" ? "silent" : "interactive";
148
+ if (raw.mode !== void 0 && raw.mode !== "silent" && raw.mode !== "interactive") return null;
149
+ if (typeof raw.subscribe !== "boolean" || typeof raw.digest !== "string" || !HEX64.test(raw.digest)) return null;
150
+ if (typeof raw.expires !== "number" || !Number.isFinite(raw.expires) || raw.expires <= now) return null;
151
+ const returnTo = typeof raw.returnTo === "string" ? raw.returnTo : "";
152
+ if (returnTo && returnPath(returnTo, mode) !== returnTo) return null;
153
+ const provider = raw.provider === "google" || raw.provider === "email" ? raw.provider : void 0;
154
+ const tx = typeof raw.tx === "string" && /^[a-f0-9]{1,64}$/.test(raw.tx) ? raw.tx : void 0;
155
+ return { digest: raw.digest, subscribe: raw.subscribe && mode === "interactive", expires: raw.expires, returnTo, mode, provider, tx };
156
+ }
157
+ function sealPendingSession(pending, key, now = Date.now()) {
158
+ return seal("reader-pending:v1", { ...pending, until: now + READER_SSO_PENDING_SECONDS * 1e3 }, key);
159
+ }
160
+ function openPendingSession(cookie, key, now = Date.now()) {
161
+ const raw = open("reader-pending:v1", cookie, key);
162
+ if (!raw || typeof raw.until !== "number" || raw.until <= now) return null;
163
+ if (typeof raw.token !== "string" || !SESSION_TOKEN.test(raw.token) || typeof raw.expires !== "number" || raw.expires <= now) return null;
164
+ if (typeof raw.userId !== "string" || !raw.userId) return null;
165
+ return {
166
+ token: raw.token,
167
+ expires: raw.expires,
168
+ userId: raw.userId,
169
+ name: typeof raw.name === "string" ? raw.name : "",
170
+ mode: raw.mode === "silent" ? "silent" : "interactive",
171
+ method: raw.method === "google" || raw.method === "email" || raw.method === "link" ? raw.method : "unknown",
172
+ returnTo: localReturnPath(raw.returnTo),
173
+ subscribe: raw.subscribe === true,
174
+ tx: typeof raw.tx === "string" ? raw.tx : void 0
175
+ };
176
+ }
177
+ function authorizeUrl(value, state, callback, mode, issuerOrigin = LEDVERY_ISSUER_ORIGIN) {
178
+ const url = new URL(value);
179
+ 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");
180
+ return url.toString();
181
+ }
182
+ async function startSignIn(config, input) {
183
+ const c = resolve(config);
184
+ const mode = input.mode === "silent" ? "silent" : "interactive";
185
+ const next = c.returnPath(input.next ?? "", mode);
186
+ const redirect = next || (mode === "silent" ? "/" : c.accountPath);
187
+ const provider = mode === "silent" ? void 0 : input.provider === "email" ? "email" : "google";
188
+ if (c.enabled === false) {
189
+ return { ok: false, status: 503, error: "Sign-in is temporarily unavailable. Please try again shortly.", reason: "disabled", redirect, cookies: [] };
190
+ }
191
+ if (provider === "email" && !c.emailSignIn) {
192
+ return { ok: false, status: 404, error: "Email sign-in is not available on this publication yet.", reason: "provider", redirect, cookies: [] };
193
+ }
194
+ const staleSession = [];
195
+ if (input.cookies(c.names.session)) {
196
+ const current = await currentUser(c, input.cookies(c.names.session));
197
+ if (current && current.email_verified) {
198
+ if (mode === "silent") return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
199
+ } else {
200
+ staleSession.push(clear(c.names.session), clear(c.names.pending));
201
+ }
202
+ }
203
+ if (mode === "silent") {
204
+ if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked)) {
205
+ return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
206
+ }
207
+ }
208
+ const tx = c.random();
209
+ const callback = c.origin + c.callbackPath;
210
+ const query = new URLSearchParams({ redirect_uri: callback });
211
+ if (mode === "silent") query.set("prompt", "none");
212
+ else {
213
+ query.set("prompt", "select_account");
214
+ query.set("upstream", provider === "email" ? "scalemule_consumer" : "google");
215
+ }
216
+ try {
217
+ const response = await c.request(`/v1/auth/oauth/ledvery/authorize?${query}`, { method: "GET" });
218
+ if (failed(response)) throw new Error("authorize");
219
+ const value = payload(response);
220
+ if (typeof value.url !== "string" || typeof value.state !== "string" || !value.state) throw new Error("authorize");
221
+ const url = authorizeUrl(value.url, value.state, callback, mode, c.issuerOrigin);
222
+ const cookies = [
223
+ ...staleSession,
224
+ { 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 }
225
+ ];
226
+ if (mode === "interactive") cookies.push(clear(c.names.signedOut), clear(c.names.ssoChecked));
227
+ emit(c, { name: "reader_signin_started", properties: { method: provider ?? "unknown", mode, transaction: tx } });
228
+ return { ok: true, url, tx, cookies };
229
+ } catch {
230
+ if (mode === "silent") {
231
+ emit(c, { name: "reader_sso_check", properties: { outcome: "error", mode, transaction: tx } });
232
+ return { ok: false, status: 503, error: "", reason: "unavailable", redirect, cookies: [...staleSession, { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS }] };
233
+ }
234
+ emit(c, { name: "reader_signin_failed", properties: { reason: "unavailable", mode, transaction: tx } });
235
+ return { ok: false, status: 503, error: "Ledvery sign-in is temporarily unavailable. Please try again shortly or use your existing reader account.", reason: "unavailable", redirect, cookies: [] };
236
+ }
237
+ }
238
+ async function currentUser(c, token) {
239
+ if (!token || !SESSION_TOKEN.test(token)) return null;
240
+ const response = await c.request("/v1/auth/me", { method: "GET", token });
241
+ if (failed(response)) return null;
242
+ const me = payload(response);
243
+ if (typeof me.id !== "string" || !me.id) return null;
244
+ return {
245
+ id: me.id,
246
+ email_verified: me.email_verified === true,
247
+ full_name: typeof me.full_name === "string" ? me.full_name.trim().slice(0, 150) : "",
248
+ locale: typeof me.locale === "string" && me.locale.length <= 35 ? me.locale : void 0,
249
+ time_zone: typeof me.time_zone === "string" && me.time_zone.length <= 64 ? me.time_zone : void 0
250
+ };
251
+ }
252
+ function issuedSession(response, now) {
253
+ const value = payload(response);
254
+ const token = typeof value.session_token === "string" ? value.session_token : "";
255
+ const expiresAt = new Date(typeof value.expires_at === "string" || typeof value.expires_at === "number" ? value.expires_at : NaN);
256
+ if (!SESSION_TOKEN.test(token) || !Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= now) throw new Error("Invalid reader session");
257
+ return { token, expiresAt, isNew: value.is_new_user === true };
258
+ }
259
+ function precedence(current, incoming) {
260
+ if (!current) return "replace";
261
+ return current.id === incoming.id ? "renew" : "switch";
262
+ }
263
+ async function completeSignIn(config, input) {
264
+ const c = resolve(config);
265
+ const now = c.now();
266
+ const query = new URL(String(input.url), c.origin).searchParams;
267
+ const state = query.get("state");
268
+ const code = query.get("code");
269
+ const error = query.get("error");
270
+ const flow = openReaderFlow(input.cookies(c.names.flow), c.key, now, c.returnPath);
271
+ const cookies = [clear(c.names.flow)];
272
+ const negativeCache = { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS };
273
+ const redirect = (location, outcome) => ({ kind: "redirect", location, cookies, outcome, flow });
274
+ if (!flow || !validState(state, flow.digest)) {
275
+ cookies.push(negativeCache);
276
+ emit(c, { name: "reader_signin_failed", properties: { reason: "state", mode: flow?.mode ?? "interactive", transaction: flow?.tx } });
277
+ return redirect(c.retryPath(flow), "state");
278
+ }
279
+ const silent = flow.mode === "silent";
280
+ const quiet = flow.returnTo || "/";
281
+ const method = flow.provider ?? "unknown";
282
+ if (error !== null) {
283
+ if (silent) {
284
+ const outcome = SILENT_NEGATIVE_ERRORS.includes(error) ? error : "error";
285
+ cookies.push(negativeCache);
286
+ emit(c, { name: "reader_sso_check", properties: { outcome, mode: "silent", transaction: flow.tx } });
287
+ return redirect(quiet, outcome);
288
+ }
289
+ const reason = error === "access_denied" ? "cancelled" : "provider_error";
290
+ emit(c, { name: "reader_signin_failed", properties: { reason, mode: "interactive", transaction: flow.tx } });
291
+ return redirect(c.retryPath(flow), reason);
292
+ }
293
+ if (!code || code.length > 4096) {
294
+ if (silent) cookies.push(negativeCache);
295
+ emit(c, { name: "reader_signin_failed", properties: { reason: "state", mode: flow.mode, transaction: flow.tx } });
296
+ return redirect(silent ? quiet : c.retryPath(flow), "state");
297
+ }
298
+ try {
299
+ const exchange = await c.request(`/v1/auth/oauth/ledvery/callback?${new URLSearchParams({ code, state })}`, { method: "GET" });
300
+ if (exchange.status === 409 && errorCode(exchange) === "LINK_REQUIRED") {
301
+ const challenge = payload(exchange);
302
+ const challengeId = typeof challenge.challenge_id === "string" && CHALLENGE_ID.test(challenge.challenge_id) ? challenge.challenge_id : "";
303
+ const hint = typeof challenge.email_hint === "string" && challenge.email_hint.length <= 254 ? challenge.email_hint : "";
304
+ if (silent || !challengeId) {
305
+ cookies.push(negativeCache);
306
+ emit(c, { name: "reader_sso_check", properties: { outcome: "link_required", mode: flow.mode, transaction: flow.tx } });
307
+ return redirect(silent ? quiet : c.retryPath(flow), "link_required");
308
+ }
309
+ emit(c, { name: "reader_sso_check", properties: { outcome: "link_required", mode: "interactive", transaction: flow.tx } });
310
+ const link = new URLSearchParams({ link: challengeId });
311
+ if (hint) link.set("hint", hint);
312
+ if (flow.returnTo) link.set("next", flow.returnTo);
313
+ if (flow.subscribe) link.set("subscribe", "1");
314
+ return redirect(`${c.accountPath}?${link}`, "link_required");
315
+ }
316
+ if (failed(exchange)) throw new Error("exchange");
317
+ const session = issuedSession(exchange, now);
318
+ return await establish(c, { session, flow, method, cookies, input, negativeCache });
319
+ } catch (e) {
320
+ const reason = e instanceof Error && e.message === "unverified" ? "unverified" : "exchange";
321
+ if (silent) cookies.push(negativeCache);
322
+ emit(c, { name: "reader_signin_failed", properties: { reason, mode: flow.mode, transaction: flow.tx } });
323
+ return redirect(silent ? quiet : c.retryPath(flow), reason);
324
+ }
325
+ }
326
+ async function establish(c, args) {
327
+ const { session, flow, method, cookies } = args;
328
+ const user = await currentUser(c, session.token);
329
+ if (!user || !user.email_verified) throw new Error("unverified");
330
+ const current = await currentUser(c, args.input.cookies(c.names.session));
331
+ const decision = precedence(current, user);
332
+ if (decision === "switch") {
333
+ cookies.push({
334
+ name: c.names.pending,
335
+ value: sealPendingSession({ token: session.token, expires: session.expiresAt.getTime(), userId: user.id, name: user.full_name, mode: flow.mode, method, returnTo: flow.returnTo, subscribe: flow.subscribe, tx: flow.tx }, c.key, c.now()),
336
+ maxAge: READER_SSO_PENDING_SECONDS
337
+ });
338
+ if (flow.mode === "silent") cookies.push(args.negativeCache);
339
+ emit(c, { name: "reader_sso_check", properties: { outcome: "switch_offered", mode: flow.mode, transaction: flow.tx } });
340
+ return { kind: "redirect", location: `${c.accountPath}?switch=1`, cookies, outcome: "switch_offered", flow };
341
+ }
342
+ cookies.push({ name: c.names.session, value: session.token, expires: session.expiresAt }, clear(c.names.signedOut), clear(c.names.ssoChecked), clear(c.names.pending));
343
+ emit(c, { name: "reader_signin_completed", properties: { method, mode: flow.mode, is_new: session.isNew ? "true" : "false", transaction: flow.tx } });
344
+ return { kind: "signed-in", location: flow.returnTo || c.accountPath, cookies, token: session.token, expiresAt: session.expiresAt, user, flow, isNew: session.isNew };
345
+ }
346
+ function linkFailureMessage(code) {
347
+ switch (code) {
348
+ case "MFA_REQUIRED":
349
+ return { error: "Enter the code from your authenticator app (or a backup code) to finish connecting your account.", mfaRequired: true };
350
+ case "MFA_SETUP_REQUIRED":
351
+ return { error: "Your existing account requires two-step verification. Set it up from your account page, then try again." };
352
+ case "MFA_METHOD_UNSUPPORTED":
353
+ return { error: "Your account's verification method can't be used here. Sign in with your password first, then connect from your account page." };
354
+ case "IDENTITY_ALREADY_BOUND":
355
+ return { error: "That account already signs in with a different identity. Sign in the way you did before." };
356
+ default:
357
+ return null;
358
+ }
359
+ }
360
+ async function linkAccount(config, input) {
361
+ const c = resolve(config);
362
+ const challengeId = typeof input.challenge_id === "string" && CHALLENGE_ID.test(input.challenge_id) ? input.challenge_id : "";
363
+ const password = typeof input.password === "string" && input.password.length > 0 && input.password.length <= 1024 ? input.password : "";
364
+ if (!challengeId || !password) {
365
+ emit(c, { name: "reader_signin_failed", properties: { reason: "link_invalid", mode: "interactive" } });
366
+ return { ok: false, status: 400, error: "Enter the password of your existing reader account.", cookies: [] };
367
+ }
368
+ const next = c.returnPath(input.next ?? "", "interactive");
369
+ const mfaCode = typeof input.mfa_code === "string" ? input.mfa_code.trim() : "";
370
+ if (mfaCode && (mfaCode.length > 32 || !/^[A-Za-z0-9-]+$/.test(mfaCode))) {
371
+ return { ok: false, status: 400, error: "Enter the code exactly as your authenticator app shows it.", cookies: [], mfaRequired: true };
372
+ }
373
+ try {
374
+ const response = await c.request("/v1/auth/oauth/ledvery/link", { method: "POST", body: { challenge_id: challengeId, password, ...mfaCode ? { mfa_code: mfaCode } : {} } });
375
+ if (failed(response)) {
376
+ const specific = linkFailureMessage(errorCode(response));
377
+ if (specific) {
378
+ emit(c, { name: "reader_signin_failed", properties: { reason: specific.mfaRequired ? "mfa_required" : "link_invalid", mode: "interactive" } });
379
+ return { ok: false, status: specific.mfaRequired ? 401 : 409, cookies: [], ...specific };
380
+ }
381
+ emit(c, { name: "reader_signin_failed", properties: { reason: response.status >= 500 ? "unavailable" : "link_invalid", mode: "interactive" } });
382
+ return response.status >= 500 ? { ok: false, status: 503, error: "The publication is temporarily unavailable. Please try again.", cookies: [] } : { ok: false, status: 400, error: "We couldn\u2019t link the accounts with those details. Check the password or start the sign-in again.", cookies: [] };
383
+ }
384
+ const session = issuedSession(response, c.now());
385
+ const flow = { digest: "", subscribe: input.subscribe === true, expires: 0, returnTo: next, mode: "interactive", provider: void 0 };
386
+ const result = await establish(c, { session, flow, method: "link", cookies: [], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
387
+ if (result.kind === "signed-in") emit(c, { name: "reader_account_linked", properties: {} });
388
+ return result.kind === "signed-in" ? { ok: true, switch: false, cookies: result.cookies, token: result.token, expiresAt: result.expiresAt, user: result.user, location: result.location } : { ok: true, switch: true, cookies: result.cookies, location: result.location };
389
+ } catch {
390
+ emit(c, { name: "reader_signin_failed", properties: { reason: "exchange", mode: "interactive" } });
391
+ return { ok: false, status: 503, error: "Sign-in could not be completed. Please start again.", cookies: [] };
392
+ }
393
+ }
394
+ async function pendingSwitch(config, cookies) {
395
+ const c = resolve(config);
396
+ const pending = openPendingSession(cookies(c.names.pending), c.key, c.now());
397
+ if (!pending) return null;
398
+ const current = await currentUser(c, cookies(c.names.session));
399
+ return { current: current?.full_name ?? "", pending: pending.name };
400
+ }
401
+ async function switchAccount(config, input) {
402
+ const c = resolve(config);
403
+ const pending = openPendingSession(input.cookies(c.names.pending), c.key, c.now());
404
+ const cookies = [clear(c.names.pending)];
405
+ if (!pending) return { ok: false, status: 410, error: "That sign-in has expired. Please start again.", cookies };
406
+ const logout = async (token) => {
407
+ if (!token || !SESSION_TOKEN.test(token)) return;
408
+ try {
409
+ await c.request("/v1/auth/logout", { method: "POST", body: { session_token: token }, token });
410
+ } catch {
411
+ }
412
+ };
413
+ if (input.choice === "stay") {
414
+ await logout(pending.token);
415
+ emit(c, { name: "reader_sso_check", properties: { outcome: "switch_declined", mode: pending.mode, transaction: pending.tx } });
416
+ return { ok: true, choice: "stay", cookies, location: pending.returnTo || c.accountPath };
417
+ }
418
+ if (input.choice !== "continue") return { ok: false, status: 400, error: "Choose which account to continue with.", cookies: [] };
419
+ const user = await currentUser(c, pending.token);
420
+ if (!user || !user.email_verified || user.id !== pending.userId) {
421
+ emit(c, { name: "reader_signin_failed", properties: { reason: "session", mode: pending.mode, transaction: pending.tx } });
422
+ return { ok: false, status: 410, error: "That sign-in is no longer valid. Please start again.", cookies };
423
+ }
424
+ await logout(input.cookies(c.names.session));
425
+ cookies.push({ name: c.names.session, value: pending.token, expires: new Date(pending.expires) }, clear(c.names.signedOut), clear(c.names.ssoChecked));
426
+ emit(c, { name: "reader_signin_completed", properties: { method: "switch", mode: pending.mode, is_new: "false", transaction: pending.tx } });
427
+ return { ok: true, choice: "continue", cookies, token: pending.token, location: pending.returnTo || c.accountPath, subscribe: pending.subscribe && pending.mode === "interactive" };
428
+ }
429
+ async function signOut(config, input) {
430
+ const c = resolve(config);
431
+ const token = input.cookies(c.names.session);
432
+ try {
433
+ if (token && SESSION_TOKEN.test(token)) await c.request("/v1/auth/logout", { method: "POST", body: { session_token: token }, token });
434
+ } catch {
435
+ }
436
+ const state = c.random();
437
+ const endSession = new URL("/end-session", c.issuerOrigin);
438
+ endSession.searchParams.set("client_id", c.clientId);
439
+ endSession.searchParams.set("post_logout_redirect_uri", `${c.origin}${c.accountPath}?signed_out=1`);
440
+ endSession.searchParams.set("state", state);
441
+ emit(c, { name: "reader_signed_out", properties: { method: "explicit" } });
442
+ return {
443
+ cookies: [
444
+ clear(c.names.session),
445
+ clear(c.names.flow),
446
+ clear(c.names.newsletter),
447
+ clear(c.names.pending),
448
+ clear(c.names.ssoChecked),
449
+ { name: c.names.signedOut, value: "1", maxAge: READER_SSO_SIGNED_OUT_SECONDS },
450
+ ...input.extraCookies ?? []
451
+ ],
452
+ endSessionUrl: endSession.toString(),
453
+ state
454
+ };
455
+ }
456
+ async function sessionSummary(config, cookies) {
457
+ const c = resolve(config);
458
+ const user = await currentUser(c, cookies(c.names.session));
459
+ if (!user || !user.email_verified) return { authenticated: false };
460
+ return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone };
461
+ }
462
+ async function sessionSummaryWithCookies(config, cookies) {
463
+ const c = resolve(config);
464
+ const present = cookies(c.names.session);
465
+ const summary = await sessionSummary(config, cookies);
466
+ return { summary, cookies: present && !summary.authenticated ? [clear(c.names.session), clear(c.names.pending)] : [] };
467
+ }
468
+ function readerSsoBootFlags(cookies, names) {
469
+ return {
470
+ hasSession: Boolean(cookies(names.session)),
471
+ suppressed: Boolean(cookies(names.signedOut) || cookies(names.ssoChecked) || cookies(names.pending))
472
+ };
473
+ }
474
+
475
+ exports.LEDVERY_ISSUER_ORIGIN = LEDVERY_ISSUER_ORIGIN;
476
+ exports.READER_SSO_EVENTS = READER_SSO_EVENTS;
477
+ exports.READER_SSO_FLOW_SECONDS = READER_SSO_FLOW_SECONDS;
478
+ exports.READER_SSO_NEGATIVE_CACHE_SECONDS = READER_SSO_NEGATIVE_CACHE_SECONDS;
479
+ exports.READER_SSO_PENDING_SECONDS = READER_SSO_PENDING_SECONDS;
480
+ exports.READER_SSO_PROPERTY_KEYS = READER_SSO_PROPERTY_KEYS;
481
+ exports.READER_SSO_SIGNED_OUT_SECONDS = READER_SSO_SIGNED_OUT_SECONDS;
482
+ exports.SILENT_NEGATIVE_ERRORS = SILENT_NEGATIVE_ERRORS;
483
+ exports.applyCookies = applyCookies;
484
+ exports.authorizeUrl = authorizeUrl;
485
+ exports.completeSignIn = completeSignIn;
486
+ exports.cookieReader = cookieReader;
487
+ exports.linkAccount = linkAccount;
488
+ exports.linkFailureMessage = linkFailureMessage;
489
+ exports.localReturnPath = localReturnPath;
490
+ exports.openPendingSession = openPendingSession;
491
+ exports.openReaderFlow = openReaderFlow;
492
+ exports.pendingSwitch = pendingSwitch;
493
+ exports.precedence = precedence;
494
+ exports.readerSsoBootFlags = readerSsoBootFlags;
495
+ exports.readerSsoCookieNames = readerSsoCookieNames;
496
+ exports.readerSsoCookieOptions = readerSsoCookieOptions;
497
+ exports.sealPendingSession = sealPendingSession;
498
+ exports.sealReaderFlow = sealReaderFlow;
499
+ exports.sessionSummary = sessionSummary;
500
+ exports.sessionSummaryWithCookies = sessionSummaryWithCookies;
501
+ exports.setCookieHeader = setCookieHeader;
502
+ exports.signOut = signOut;
503
+ exports.startSignIn = startSignIn;
504
+ exports.stateDigest = stateDigest;
505
+ exports.switchAccount = switchAccount;
506
+ exports.validState = validState;