@scalemule/news-publication 0.0.90 → 0.0.92

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.
@@ -20,6 +20,7 @@ var READER_SSO_FLOW_SECONDS = 600;
20
20
  var READER_SSO_NEGATIVE_CACHE_SECONDS = 300;
21
21
  var READER_SSO_SIGNED_OUT_SECONDS = 86400;
22
22
  var READER_SSO_PENDING_SECONDS = 600;
23
+ var READER_SSO_LINK_SECONDS = 600;
23
24
  var LEDVERY_ISSUER_ORIGIN = "https://api.ledvery.com";
24
25
  var SILENT_NEGATIVE_ERRORS = ["login_required", "consent_required", "interaction_required", "account_selection_required"];
25
26
  var COOKIE_PREFIX = /^__Host-[A-Za-z0-9_-]{1,60}$/;
@@ -35,7 +36,8 @@ function readerSsoCookieNames(prefix) {
35
36
  ssoChecked: `${prefix}-sso-checked`,
36
37
  signedOut: `${prefix}-signed-out`,
37
38
  pending: `${prefix}-sso-pending`,
38
- linkCode: `${prefix}-link-code`
39
+ linkCode: `${prefix}-link-code`,
40
+ linkFlow: `${prefix}-sso-link`
39
41
  };
40
42
  }
41
43
  var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
@@ -160,6 +162,14 @@ function openReaderFlow(cookie, key, now = Date.now(), returnPath = (next) => lo
160
162
  const tx = typeof raw.tx === "string" && /^[a-f0-9]{1,64}$/.test(raw.tx) ? raw.tx : void 0;
161
163
  return { digest: raw.digest, subscribe: raw.subscribe && mode === "interactive", expires: raw.expires, returnTo, mode, provider, tx };
162
164
  }
165
+ function sealLinkChallenge(challengeId, key, now = Date.now()) {
166
+ if (!CHALLENGE_ID.test(challengeId)) throw new Error("Invalid reader link challenge");
167
+ return seal("reader-link:v1", { challengeId, expires: now + READER_SSO_LINK_SECONDS * 1e3 }, key);
168
+ }
169
+ function openLinkChallenge(cookie, key, now = Date.now()) {
170
+ const raw = open("reader-link:v1", cookie, key);
171
+ return raw && typeof raw.challengeId === "string" && CHALLENGE_ID.test(raw.challengeId) && typeof raw.expires === "number" && Number.isFinite(raw.expires) && raw.expires > now ? raw.challengeId : null;
172
+ }
163
173
  function sealPendingSession(pending, key, now = Date.now()) {
164
174
  return seal("reader-pending:v1", { ...pending, until: now + READER_SSO_PENDING_SECONDS * 1e3 }, key);
165
175
  }
@@ -215,7 +225,7 @@ async function startSignIn(config, input) {
215
225
  }
216
226
  }
217
227
  if (mode === "silent") {
218
- if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
228
+ if (input.cookies(c.names.signedOut)) {
219
229
  return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
220
230
  }
221
231
  }
@@ -237,7 +247,7 @@ async function startSignIn(config, input) {
237
247
  ...staleSession,
238
248
  { 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 }
239
249
  ];
240
- if (mode === "interactive") cookies.push(clear(c.names.signedOut), clear(c.names.ssoChecked));
250
+ if (mode === "interactive") cookies.push(clear(c.names.signedOut), clear(c.names.ssoChecked), clear(c.names.linkFlow), clear(c.names.linkCode));
241
251
  emit(c, { name: "reader_signin_started", properties: { method: provider ?? "unknown", mode, transaction: tx } });
242
252
  return { ok: true, url, tx, cookies };
243
253
  } catch {
@@ -326,6 +336,7 @@ async function completeSignIn(config, input) {
326
336
  if (hint) link.set("hint", hint);
327
337
  if (flow.returnTo) link.set("next", flow.returnTo);
328
338
  if (flow.subscribe) link.set("subscribe", "1");
339
+ cookies.push({ name: c.names.linkFlow, value: sealLinkChallenge(challengeId, c.key, now), maxAge: READER_SSO_LINK_SECONDS });
329
340
  return redirect(`${c.accountPath}?${link}`, "link_required");
330
341
  }
331
342
  if (failed(exchange)) throw new Error("exchange");
@@ -410,7 +421,11 @@ async function linkAccount(config, input) {
410
421
  const c = resolve(config);
411
422
  const challengeId = typeof input.challenge_id === "string" && CHALLENGE_ID.test(input.challenge_id) ? input.challenge_id : "";
412
423
  const password = typeof input.password === "string" && input.password.length > 0 && input.password.length <= 1024 ? input.password : "";
413
- if (!challengeId || !password) {
424
+ if (c.enabled === false || !challengeId || openLinkChallenge(input.cookies(c.names.linkFlow), c.key, c.now()) !== challengeId) {
425
+ emit(c, { name: "reader_signin_failed", properties: { reason: "link_invalid", mode: "interactive" } });
426
+ return { ok: false, status: 400, error: "Start sign-in again to connect your existing reader account.", cookies: [] };
427
+ }
428
+ if (!password) {
414
429
  emit(c, { name: "reader_signin_failed", properties: { reason: "link_invalid", mode: "interactive" } });
415
430
  return { ok: false, status: 400, error: "Enter the password of your existing reader account.", cookies: [] };
416
431
  }
@@ -439,7 +454,7 @@ async function linkAccount(config, input) {
439
454
  }
440
455
  const session = issuedSession(response, c.now());
441
456
  const flow = { digest: "", subscribe: input.subscribe === true, expires: 0, returnTo: next, mode: "interactive", provider: void 0 };
442
- const result = await establish(c, { session, flow, method: "link", cookies: emailChallenge ? [clear(c.names.linkCode)] : [], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
457
+ const result = await establish(c, { session, flow, method: "link", cookies: [clear(c.names.linkFlow), ...emailChallenge ? [clear(c.names.linkCode)] : []], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
443
458
  if (result.kind === "signed-in") emit(c, { name: "reader_account_linked", properties: {} });
444
459
  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 };
445
460
  } catch {
@@ -507,6 +522,8 @@ async function signOut(config, input) {
507
522
  clear(c.names.newsletter),
508
523
  clear(c.names.pending),
509
524
  clear(c.names.ssoChecked),
525
+ clear(c.names.linkFlow),
526
+ clear(c.names.linkCode),
510
527
  { name: c.names.signedOut, value: "1", maxAge: READER_SSO_SIGNED_OUT_SECONDS },
511
528
  ...input.extraCookies ?? []
512
529
  ],
@@ -536,6 +553,7 @@ function readerSsoBootFlags(cookies, names) {
536
553
  exports.LEDVERY_ISSUER_ORIGIN = LEDVERY_ISSUER_ORIGIN;
537
554
  exports.READER_SSO_EVENTS = READER_SSO_EVENTS;
538
555
  exports.READER_SSO_FLOW_SECONDS = READER_SSO_FLOW_SECONDS;
556
+ exports.READER_SSO_LINK_SECONDS = READER_SSO_LINK_SECONDS;
539
557
  exports.READER_SSO_NEGATIVE_CACHE_SECONDS = READER_SSO_NEGATIVE_CACHE_SECONDS;
540
558
  exports.READER_SSO_PENDING_SECONDS = READER_SSO_PENDING_SECONDS;
541
559
  exports.READER_SSO_PROPERTY_KEYS = READER_SSO_PROPERTY_KEYS;
@@ -549,6 +567,7 @@ exports.linkAccount = linkAccount;
549
567
  exports.linkFailureMessage = linkFailureMessage;
550
568
  exports.localReturnPath = localReturnPath;
551
569
  exports.networkHandoffReturnPath = networkHandoffReturnPath;
570
+ exports.openLinkChallenge = openLinkChallenge;
552
571
  exports.openPendingSession = openPendingSession;
553
572
  exports.openReaderFlow = openReaderFlow;
554
573
  exports.pendingSwitch = pendingSwitch;
@@ -556,6 +575,7 @@ exports.precedence = precedence;
556
575
  exports.readerSsoBootFlags = readerSsoBootFlags;
557
576
  exports.readerSsoCookieNames = readerSsoCookieNames;
558
577
  exports.readerSsoCookieOptions = readerSsoCookieOptions;
578
+ exports.sealLinkChallenge = sealLinkChallenge;
559
579
  exports.sealPendingSession = sealPendingSession;
560
580
  exports.sealReaderFlow = sealReaderFlow;
561
581
  exports.sessionSummary = sessionSummary;
@@ -7,6 +7,7 @@ declare const READER_SSO_FLOW_SECONDS = 600;
7
7
  declare const READER_SSO_NEGATIVE_CACHE_SECONDS = 300;
8
8
  declare const READER_SSO_SIGNED_OUT_SECONDS = 86400;
9
9
  declare const READER_SSO_PENDING_SECONDS = 600;
10
+ declare const READER_SSO_LINK_SECONDS = 600;
10
11
  declare const LEDVERY_ISSUER_ORIGIN = "https://api.ledvery.com";
11
12
  /** Ledvery results that mean "no silent session here"; never authentication failures. */
12
13
  declare const SILENT_NEGATIVE_ERRORS: readonly ["login_required", "consent_required", "interaction_required", "account_selection_required"];
@@ -25,6 +26,8 @@ type ReaderSsoCookieNames = {
25
26
  pending: string;
26
27
  /** Linking onto an unconfirmed account: the platform's emailed-code challenge token (10 minutes). */
27
28
  linkCode: string;
29
+ /** Signed proof that this browser received the account-link challenge from our callback. */
30
+ linkFlow: string;
28
31
  };
29
32
  /** Cookie names derived from a `__Host-` prefix, e.g. `__Host-media` → `__Host-media-reader`. */
30
33
  declare function readerSsoCookieNames(prefix: string): ReaderSsoCookieNames;
@@ -158,6 +161,9 @@ declare function sealReaderFlow(input: {
158
161
  tx?: string;
159
162
  }, key: string, now?: number): string;
160
163
  declare function openReaderFlow(cookie: string | undefined, key: string, now?: number, returnPath?: (next: string, mode: ReaderSsoMode) => string): ReaderSsoFlow | null;
164
+ /** A URL's challenge ID is not enough to start linking an existing account. */
165
+ declare function sealLinkChallenge(challengeId: string, key: string, now?: number): string;
166
+ declare function openLinkChallenge(cookie: string | undefined, key: string, now?: number): string | null;
161
167
  /** A verified session the reader has not yet chosen over their current one. */
162
168
  type PendingSession = {
163
169
  token: string;
@@ -327,4 +333,4 @@ declare function readerSsoBootFlags(cookies: CookieReader, names: ReaderSsoCooki
327
333
  suppressed: boolean;
328
334
  };
329
335
 
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 };
336
+ export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_LINK_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, openLinkChallenge, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealLinkChallenge, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
@@ -7,6 +7,7 @@ declare const READER_SSO_FLOW_SECONDS = 600;
7
7
  declare const READER_SSO_NEGATIVE_CACHE_SECONDS = 300;
8
8
  declare const READER_SSO_SIGNED_OUT_SECONDS = 86400;
9
9
  declare const READER_SSO_PENDING_SECONDS = 600;
10
+ declare const READER_SSO_LINK_SECONDS = 600;
10
11
  declare const LEDVERY_ISSUER_ORIGIN = "https://api.ledvery.com";
11
12
  /** Ledvery results that mean "no silent session here"; never authentication failures. */
12
13
  declare const SILENT_NEGATIVE_ERRORS: readonly ["login_required", "consent_required", "interaction_required", "account_selection_required"];
@@ -25,6 +26,8 @@ type ReaderSsoCookieNames = {
25
26
  pending: string;
26
27
  /** Linking onto an unconfirmed account: the platform's emailed-code challenge token (10 minutes). */
27
28
  linkCode: string;
29
+ /** Signed proof that this browser received the account-link challenge from our callback. */
30
+ linkFlow: string;
28
31
  };
29
32
  /** Cookie names derived from a `__Host-` prefix, e.g. `__Host-media` → `__Host-media-reader`. */
30
33
  declare function readerSsoCookieNames(prefix: string): ReaderSsoCookieNames;
@@ -158,6 +161,9 @@ declare function sealReaderFlow(input: {
158
161
  tx?: string;
159
162
  }, key: string, now?: number): string;
160
163
  declare function openReaderFlow(cookie: string | undefined, key: string, now?: number, returnPath?: (next: string, mode: ReaderSsoMode) => string): ReaderSsoFlow | null;
164
+ /** A URL's challenge ID is not enough to start linking an existing account. */
165
+ declare function sealLinkChallenge(challengeId: string, key: string, now?: number): string;
166
+ declare function openLinkChallenge(cookie: string | undefined, key: string, now?: number): string | null;
161
167
  /** A verified session the reader has not yet chosen over their current one. */
162
168
  type PendingSession = {
163
169
  token: string;
@@ -327,4 +333,4 @@ declare function readerSsoBootFlags(cookies: CookieReader, names: ReaderSsoCooki
327
333
  suppressed: boolean;
328
334
  };
329
335
 
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 };
336
+ export { type CompleteSignInInput, type CompleteSignInResult, type CookieInstruction, type CookieReader, LEDVERY_ISSUER_ORIGIN, type LinkAccountInput, type LinkAccountResult, type PendingSession, READER_SSO_FLOW_SECONDS, READER_SSO_LINK_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, openLinkChallenge, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealLinkChallenge, sealPendingSession, sealReaderFlow, sessionSummary, sessionSummaryWithCookies, setCookieHeader, signOut, startSignIn, stateDigest, switchAccount, validState };
@@ -5,6 +5,7 @@ var READER_SSO_FLOW_SECONDS = 600;
5
5
  var READER_SSO_NEGATIVE_CACHE_SECONDS = 300;
6
6
  var READER_SSO_SIGNED_OUT_SECONDS = 86400;
7
7
  var READER_SSO_PENDING_SECONDS = 600;
8
+ var READER_SSO_LINK_SECONDS = 600;
8
9
  var LEDVERY_ISSUER_ORIGIN = "https://api.ledvery.com";
9
10
  var SILENT_NEGATIVE_ERRORS = ["login_required", "consent_required", "interaction_required", "account_selection_required"];
10
11
  var COOKIE_PREFIX = /^__Host-[A-Za-z0-9_-]{1,60}$/;
@@ -20,7 +21,8 @@ function readerSsoCookieNames(prefix) {
20
21
  ssoChecked: `${prefix}-sso-checked`,
21
22
  signedOut: `${prefix}-signed-out`,
22
23
  pending: `${prefix}-sso-pending`,
23
- linkCode: `${prefix}-link-code`
24
+ linkCode: `${prefix}-link-code`,
25
+ linkFlow: `${prefix}-sso-link`
24
26
  };
25
27
  }
26
28
  var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
@@ -145,6 +147,14 @@ function openReaderFlow(cookie, key, now = Date.now(), returnPath = (next) => lo
145
147
  const tx = typeof raw.tx === "string" && /^[a-f0-9]{1,64}$/.test(raw.tx) ? raw.tx : void 0;
146
148
  return { digest: raw.digest, subscribe: raw.subscribe && mode === "interactive", expires: raw.expires, returnTo, mode, provider, tx };
147
149
  }
150
+ function sealLinkChallenge(challengeId, key, now = Date.now()) {
151
+ if (!CHALLENGE_ID.test(challengeId)) throw new Error("Invalid reader link challenge");
152
+ return seal("reader-link:v1", { challengeId, expires: now + READER_SSO_LINK_SECONDS * 1e3 }, key);
153
+ }
154
+ function openLinkChallenge(cookie, key, now = Date.now()) {
155
+ const raw = open("reader-link:v1", cookie, key);
156
+ return raw && typeof raw.challengeId === "string" && CHALLENGE_ID.test(raw.challengeId) && typeof raw.expires === "number" && Number.isFinite(raw.expires) && raw.expires > now ? raw.challengeId : null;
157
+ }
148
158
  function sealPendingSession(pending, key, now = Date.now()) {
149
159
  return seal("reader-pending:v1", { ...pending, until: now + READER_SSO_PENDING_SECONDS * 1e3 }, key);
150
160
  }
@@ -200,7 +210,7 @@ async function startSignIn(config, input) {
200
210
  }
201
211
  }
202
212
  if (mode === "silent") {
203
- if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
213
+ if (input.cookies(c.names.signedOut)) {
204
214
  return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
205
215
  }
206
216
  }
@@ -222,7 +232,7 @@ async function startSignIn(config, input) {
222
232
  ...staleSession,
223
233
  { 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 }
224
234
  ];
225
- if (mode === "interactive") cookies.push(clear(c.names.signedOut), clear(c.names.ssoChecked));
235
+ if (mode === "interactive") cookies.push(clear(c.names.signedOut), clear(c.names.ssoChecked), clear(c.names.linkFlow), clear(c.names.linkCode));
226
236
  emit(c, { name: "reader_signin_started", properties: { method: provider ?? "unknown", mode, transaction: tx } });
227
237
  return { ok: true, url, tx, cookies };
228
238
  } catch {
@@ -311,6 +321,7 @@ async function completeSignIn(config, input) {
311
321
  if (hint) link.set("hint", hint);
312
322
  if (flow.returnTo) link.set("next", flow.returnTo);
313
323
  if (flow.subscribe) link.set("subscribe", "1");
324
+ cookies.push({ name: c.names.linkFlow, value: sealLinkChallenge(challengeId, c.key, now), maxAge: READER_SSO_LINK_SECONDS });
314
325
  return redirect(`${c.accountPath}?${link}`, "link_required");
315
326
  }
316
327
  if (failed(exchange)) throw new Error("exchange");
@@ -395,7 +406,11 @@ async function linkAccount(config, input) {
395
406
  const c = resolve(config);
396
407
  const challengeId = typeof input.challenge_id === "string" && CHALLENGE_ID.test(input.challenge_id) ? input.challenge_id : "";
397
408
  const password = typeof input.password === "string" && input.password.length > 0 && input.password.length <= 1024 ? input.password : "";
398
- if (!challengeId || !password) {
409
+ if (c.enabled === false || !challengeId || openLinkChallenge(input.cookies(c.names.linkFlow), c.key, c.now()) !== challengeId) {
410
+ emit(c, { name: "reader_signin_failed", properties: { reason: "link_invalid", mode: "interactive" } });
411
+ return { ok: false, status: 400, error: "Start sign-in again to connect your existing reader account.", cookies: [] };
412
+ }
413
+ if (!password) {
399
414
  emit(c, { name: "reader_signin_failed", properties: { reason: "link_invalid", mode: "interactive" } });
400
415
  return { ok: false, status: 400, error: "Enter the password of your existing reader account.", cookies: [] };
401
416
  }
@@ -424,7 +439,7 @@ async function linkAccount(config, input) {
424
439
  }
425
440
  const session = issuedSession(response, c.now());
426
441
  const flow = { digest: "", subscribe: input.subscribe === true, expires: 0, returnTo: next, mode: "interactive", provider: void 0 };
427
- const result = await establish(c, { session, flow, method: "link", cookies: emailChallenge ? [clear(c.names.linkCode)] : [], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
442
+ const result = await establish(c, { session, flow, method: "link", cookies: [clear(c.names.linkFlow), ...emailChallenge ? [clear(c.names.linkCode)] : []], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
428
443
  if (result.kind === "signed-in") emit(c, { name: "reader_account_linked", properties: {} });
429
444
  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 };
430
445
  } catch {
@@ -492,6 +507,8 @@ async function signOut(config, input) {
492
507
  clear(c.names.newsletter),
493
508
  clear(c.names.pending),
494
509
  clear(c.names.ssoChecked),
510
+ clear(c.names.linkFlow),
511
+ clear(c.names.linkCode),
495
512
  { name: c.names.signedOut, value: "1", maxAge: READER_SSO_SIGNED_OUT_SECONDS },
496
513
  ...input.extraCookies ?? []
497
514
  ],
@@ -518,4 +535,4 @@ function readerSsoBootFlags(cookies, names) {
518
535
  };
519
536
  }
520
537
 
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 };
538
+ export { LEDVERY_ISSUER_ORIGIN, READER_SSO_FLOW_SECONDS, READER_SSO_LINK_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, openLinkChallenge, openPendingSession, openReaderFlow, pendingSwitch, precedence, readerSsoBootFlags, readerSsoCookieNames, readerSsoCookieOptions, sealLinkChallenge, 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.90",
3
+ "version": "0.0.92",
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": {