@scalemule/news-publication 0.0.73 → 0.0.74
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/dist/reader-sso-client.cjs +1 -1
- package/dist/reader-sso-client.js +1 -1
- package/dist/reader-sso.cjs +21 -8
- package/dist/reader-sso.d.cts +2 -0
- package/dist/reader-sso.d.ts +2 -0
- package/dist/reader-sso.js +21 -8
- package/package.json +1 -1
|
@@ -86,7 +86,7 @@ function ReaderSsoBoot({ enabled = true, hasSession: hasSessionCookie, suppresse
|
|
|
86
86
|
if (!enabled || !hasSessionCookie || staleCookie) return;
|
|
87
87
|
let cancelled = false;
|
|
88
88
|
fetch(sessionPath, { credentials: "same-origin", cache: "no-store" }).then((response) => response.json()).then((body) => {
|
|
89
|
-
if (!cancelled && body?.authenticated !== true) setStaleCookie(true);
|
|
89
|
+
if (!cancelled && (body?.authenticated !== true || body.session_upgrade_required === true)) setStaleCookie(true);
|
|
90
90
|
}).catch(() => {
|
|
91
91
|
});
|
|
92
92
|
return () => {
|
|
@@ -84,7 +84,7 @@ function ReaderSsoBoot({ enabled = true, hasSession: hasSessionCookie, suppresse
|
|
|
84
84
|
if (!enabled || !hasSessionCookie || staleCookie) return;
|
|
85
85
|
let cancelled = false;
|
|
86
86
|
fetch(sessionPath, { credentials: "same-origin", cache: "no-store" }).then((response) => response.json()).then((body) => {
|
|
87
|
-
if (!cancelled && body?.authenticated !== true) setStaleCookie(true);
|
|
87
|
+
if (!cancelled && (body?.authenticated !== true || body.session_upgrade_required === true)) setStaleCookie(true);
|
|
88
88
|
}).catch(() => {
|
|
89
89
|
});
|
|
90
90
|
return () => {
|
package/dist/reader-sso.cjs
CHANGED
|
@@ -39,6 +39,8 @@ function readerSsoCookieNames(prefix) {
|
|
|
39
39
|
}
|
|
40
40
|
var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
|
|
41
41
|
var clear = (name) => ({ name, value: "", maxAge: 0 });
|
|
42
|
+
var keepLocalValue = (token) => "keep:" + crypto.createHash("sha256").update(token).digest("hex");
|
|
43
|
+
var keepsLocal = (checked, token) => Boolean(token && checked === keepLocalValue(token));
|
|
42
44
|
function applyCookies(jar, cookies) {
|
|
43
45
|
for (const cookie of cookies) {
|
|
44
46
|
jar.set(cookie.name, cookie.value, {
|
|
@@ -195,13 +197,13 @@ async function startSignIn(config, input) {
|
|
|
195
197
|
if (input.cookies(c.names.session)) {
|
|
196
198
|
const current = await currentUser(c, input.cookies(c.names.session));
|
|
197
199
|
if (current && current.email_verified) {
|
|
198
|
-
if (mode === "silent") return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
|
|
200
|
+
if (mode === "silent" && (!current.needsSsoUpgrade || keepsLocal(input.cookies(c.names.ssoChecked), input.cookies(c.names.session)))) return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
|
|
199
201
|
} else {
|
|
200
202
|
staleSession.push(clear(c.names.session), clear(c.names.pending));
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
205
|
if (mode === "silent") {
|
|
204
|
-
if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked)) {
|
|
206
|
+
if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
|
|
205
207
|
return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
|
|
206
208
|
}
|
|
207
209
|
}
|
|
@@ -244,9 +246,10 @@ async function currentUser(c, token) {
|
|
|
244
246
|
return {
|
|
245
247
|
id: me.id,
|
|
246
248
|
email_verified: me.email_verified === true,
|
|
247
|
-
full_name: typeof me.full_name === "string" ? me.full_name.trim().slice(0, 150) : "",
|
|
249
|
+
full_name: typeof me.full_name === "string" ? Array.from(me.full_name.trim()).slice(0, 150).join("") : "",
|
|
248
250
|
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
|
|
251
|
+
time_zone: typeof me.time_zone === "string" && me.time_zone.length <= 64 ? me.time_zone : void 0,
|
|
252
|
+
...me.sso_session_bound === false ? { needsSsoUpgrade: true } : {}
|
|
250
253
|
};
|
|
251
254
|
}
|
|
252
255
|
function issuedSession(response, now) {
|
|
@@ -326,7 +329,7 @@ async function completeSignIn(config, input) {
|
|
|
326
329
|
async function establish(c, args) {
|
|
327
330
|
const { session, flow, method, cookies } = args;
|
|
328
331
|
const user = await currentUser(c, session.token);
|
|
329
|
-
if (!user || !user.email_verified) throw new Error("unverified");
|
|
332
|
+
if (!user || !user.email_verified || user.needsSsoUpgrade) throw new Error("unverified");
|
|
330
333
|
const current = await currentUser(c, args.input.cookies(c.names.session));
|
|
331
334
|
const decision = precedence(current, user);
|
|
332
335
|
if (decision === "switch") {
|
|
@@ -337,7 +340,12 @@ async function establish(c, args) {
|
|
|
337
340
|
});
|
|
338
341
|
if (flow.mode === "silent") cookies.push(args.negativeCache);
|
|
339
342
|
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 };
|
|
343
|
+
return { kind: "redirect", location: flow.mode === "silent" ? flow.returnTo || "/" : `${c.accountPath}?switch=1`, cookies, outcome: "switch_offered", flow };
|
|
344
|
+
}
|
|
345
|
+
const previousToken = args.input.cookies(c.names.session);
|
|
346
|
+
if (current && previousToken && previousToken !== session.token) {
|
|
347
|
+
const retired = await c.request("/v1/auth/logout", { method: "POST", body: { session_token: previousToken }, token: previousToken });
|
|
348
|
+
if (failed(retired) || retired.body?.success !== true) throw new Error("session retirement failed");
|
|
341
349
|
}
|
|
342
350
|
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
351
|
emit(c, { name: "reader_signin_completed", properties: { method, mode: flow.mode, is_new: session.isNew ? "true" : "false", transaction: flow.tx } });
|
|
@@ -411,6 +419,11 @@ async function switchAccount(config, input) {
|
|
|
411
419
|
}
|
|
412
420
|
};
|
|
413
421
|
if (input.choice === "stay") {
|
|
422
|
+
const token = input.cookies(c.names.session);
|
|
423
|
+
const current = await currentUser(c, token);
|
|
424
|
+
if (token && current?.email_verified && current.needsSsoUpgrade) {
|
|
425
|
+
cookies.push({ name: c.names.ssoChecked, value: keepLocalValue(token), maxAge: 90 * 86400 });
|
|
426
|
+
}
|
|
414
427
|
await logout(pending.token);
|
|
415
428
|
emit(c, { name: "reader_sso_check", properties: { outcome: "switch_declined", mode: pending.mode, transaction: pending.tx } });
|
|
416
429
|
return { ok: true, choice: "stay", cookies, location: pending.returnTo || c.accountPath };
|
|
@@ -457,7 +470,7 @@ async function sessionSummary(config, cookies) {
|
|
|
457
470
|
const c = resolve(config);
|
|
458
471
|
const user = await currentUser(c, cookies(c.names.session));
|
|
459
472
|
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 };
|
|
473
|
+
return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone, ...user.needsSsoUpgrade && !keepsLocal(cookies(c.names.ssoChecked), cookies(c.names.session)) ? { session_upgrade_required: true } : {} };
|
|
461
474
|
}
|
|
462
475
|
async function sessionSummaryWithCookies(config, cookies) {
|
|
463
476
|
const c = resolve(config);
|
|
@@ -468,7 +481,7 @@ async function sessionSummaryWithCookies(config, cookies) {
|
|
|
468
481
|
function readerSsoBootFlags(cookies, names) {
|
|
469
482
|
return {
|
|
470
483
|
hasSession: Boolean(cookies(names.session)),
|
|
471
|
-
suppressed: Boolean(cookies(names.signedOut) || cookies(names.ssoChecked) || cookies(names.pending))
|
|
484
|
+
suppressed: Boolean(cookies(names.signedOut) || cookies(names.ssoChecked) === "1" || cookies(names.pending))
|
|
472
485
|
};
|
|
473
486
|
}
|
|
474
487
|
|
package/dist/reader-sso.d.cts
CHANGED
|
@@ -192,6 +192,7 @@ type SessionUser = {
|
|
|
192
192
|
full_name: string;
|
|
193
193
|
locale?: string;
|
|
194
194
|
time_zone?: string;
|
|
195
|
+
needsSsoUpgrade?: true;
|
|
195
196
|
};
|
|
196
197
|
/** ADR 3.2: an active session for Bob is never replaced by Alice without an explicit choice. */
|
|
197
198
|
declare function precedence(current: SessionUser | null, incoming: SessionUser): "replace" | "renew" | "switch";
|
|
@@ -298,6 +299,7 @@ type SessionSummary = {
|
|
|
298
299
|
full_name: string;
|
|
299
300
|
locale?: string;
|
|
300
301
|
time_zone?: string;
|
|
302
|
+
session_upgrade_required?: true;
|
|
301
303
|
};
|
|
302
304
|
declare function sessionSummary(config: ReaderSsoConfig, cookies: CookieReader): Promise<SessionSummary>;
|
|
303
305
|
/**
|
package/dist/reader-sso.d.ts
CHANGED
|
@@ -192,6 +192,7 @@ type SessionUser = {
|
|
|
192
192
|
full_name: string;
|
|
193
193
|
locale?: string;
|
|
194
194
|
time_zone?: string;
|
|
195
|
+
needsSsoUpgrade?: true;
|
|
195
196
|
};
|
|
196
197
|
/** ADR 3.2: an active session for Bob is never replaced by Alice without an explicit choice. */
|
|
197
198
|
declare function precedence(current: SessionUser | null, incoming: SessionUser): "replace" | "renew" | "switch";
|
|
@@ -298,6 +299,7 @@ type SessionSummary = {
|
|
|
298
299
|
full_name: string;
|
|
299
300
|
locale?: string;
|
|
300
301
|
time_zone?: string;
|
|
302
|
+
session_upgrade_required?: true;
|
|
301
303
|
};
|
|
302
304
|
declare function sessionSummary(config: ReaderSsoConfig, cookies: CookieReader): Promise<SessionSummary>;
|
|
303
305
|
/**
|
package/dist/reader-sso.js
CHANGED
|
@@ -24,6 +24,8 @@ function readerSsoCookieNames(prefix) {
|
|
|
24
24
|
}
|
|
25
25
|
var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
|
|
26
26
|
var clear = (name) => ({ name, value: "", maxAge: 0 });
|
|
27
|
+
var keepLocalValue = (token) => "keep:" + createHash("sha256").update(token).digest("hex");
|
|
28
|
+
var keepsLocal = (checked, token) => Boolean(token && checked === keepLocalValue(token));
|
|
27
29
|
function applyCookies(jar, cookies) {
|
|
28
30
|
for (const cookie of cookies) {
|
|
29
31
|
jar.set(cookie.name, cookie.value, {
|
|
@@ -180,13 +182,13 @@ async function startSignIn(config, input) {
|
|
|
180
182
|
if (input.cookies(c.names.session)) {
|
|
181
183
|
const current = await currentUser(c, input.cookies(c.names.session));
|
|
182
184
|
if (current && current.email_verified) {
|
|
183
|
-
if (mode === "silent") return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
|
|
185
|
+
if (mode === "silent" && (!current.needsSsoUpgrade || keepsLocal(input.cookies(c.names.ssoChecked), input.cookies(c.names.session)))) return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
|
|
184
186
|
} else {
|
|
185
187
|
staleSession.push(clear(c.names.session), clear(c.names.pending));
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
if (mode === "silent") {
|
|
189
|
-
if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked)) {
|
|
191
|
+
if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
|
|
190
192
|
return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
|
|
191
193
|
}
|
|
192
194
|
}
|
|
@@ -229,9 +231,10 @@ async function currentUser(c, token) {
|
|
|
229
231
|
return {
|
|
230
232
|
id: me.id,
|
|
231
233
|
email_verified: me.email_verified === true,
|
|
232
|
-
full_name: typeof me.full_name === "string" ? me.full_name.trim().slice(0, 150) : "",
|
|
234
|
+
full_name: typeof me.full_name === "string" ? Array.from(me.full_name.trim()).slice(0, 150).join("") : "",
|
|
233
235
|
locale: typeof me.locale === "string" && me.locale.length <= 35 ? me.locale : void 0,
|
|
234
|
-
time_zone: typeof me.time_zone === "string" && me.time_zone.length <= 64 ? me.time_zone : void 0
|
|
236
|
+
time_zone: typeof me.time_zone === "string" && me.time_zone.length <= 64 ? me.time_zone : void 0,
|
|
237
|
+
...me.sso_session_bound === false ? { needsSsoUpgrade: true } : {}
|
|
235
238
|
};
|
|
236
239
|
}
|
|
237
240
|
function issuedSession(response, now) {
|
|
@@ -311,7 +314,7 @@ async function completeSignIn(config, input) {
|
|
|
311
314
|
async function establish(c, args) {
|
|
312
315
|
const { session, flow, method, cookies } = args;
|
|
313
316
|
const user = await currentUser(c, session.token);
|
|
314
|
-
if (!user || !user.email_verified) throw new Error("unverified");
|
|
317
|
+
if (!user || !user.email_verified || user.needsSsoUpgrade) throw new Error("unverified");
|
|
315
318
|
const current = await currentUser(c, args.input.cookies(c.names.session));
|
|
316
319
|
const decision = precedence(current, user);
|
|
317
320
|
if (decision === "switch") {
|
|
@@ -322,7 +325,12 @@ async function establish(c, args) {
|
|
|
322
325
|
});
|
|
323
326
|
if (flow.mode === "silent") cookies.push(args.negativeCache);
|
|
324
327
|
emit(c, { name: "reader_sso_check", properties: { outcome: "switch_offered", mode: flow.mode, transaction: flow.tx } });
|
|
325
|
-
return { kind: "redirect", location: `${c.accountPath}?switch=1`, cookies, outcome: "switch_offered", flow };
|
|
328
|
+
return { kind: "redirect", location: flow.mode === "silent" ? flow.returnTo || "/" : `${c.accountPath}?switch=1`, cookies, outcome: "switch_offered", flow };
|
|
329
|
+
}
|
|
330
|
+
const previousToken = args.input.cookies(c.names.session);
|
|
331
|
+
if (current && previousToken && previousToken !== session.token) {
|
|
332
|
+
const retired = await c.request("/v1/auth/logout", { method: "POST", body: { session_token: previousToken }, token: previousToken });
|
|
333
|
+
if (failed(retired) || retired.body?.success !== true) throw new Error("session retirement failed");
|
|
326
334
|
}
|
|
327
335
|
cookies.push({ name: c.names.session, value: session.token, expires: session.expiresAt }, clear(c.names.signedOut), clear(c.names.ssoChecked), clear(c.names.pending));
|
|
328
336
|
emit(c, { name: "reader_signin_completed", properties: { method, mode: flow.mode, is_new: session.isNew ? "true" : "false", transaction: flow.tx } });
|
|
@@ -396,6 +404,11 @@ async function switchAccount(config, input) {
|
|
|
396
404
|
}
|
|
397
405
|
};
|
|
398
406
|
if (input.choice === "stay") {
|
|
407
|
+
const token = input.cookies(c.names.session);
|
|
408
|
+
const current = await currentUser(c, token);
|
|
409
|
+
if (token && current?.email_verified && current.needsSsoUpgrade) {
|
|
410
|
+
cookies.push({ name: c.names.ssoChecked, value: keepLocalValue(token), maxAge: 90 * 86400 });
|
|
411
|
+
}
|
|
399
412
|
await logout(pending.token);
|
|
400
413
|
emit(c, { name: "reader_sso_check", properties: { outcome: "switch_declined", mode: pending.mode, transaction: pending.tx } });
|
|
401
414
|
return { ok: true, choice: "stay", cookies, location: pending.returnTo || c.accountPath };
|
|
@@ -442,7 +455,7 @@ async function sessionSummary(config, cookies) {
|
|
|
442
455
|
const c = resolve(config);
|
|
443
456
|
const user = await currentUser(c, cookies(c.names.session));
|
|
444
457
|
if (!user || !user.email_verified) return { authenticated: false };
|
|
445
|
-
return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone };
|
|
458
|
+
return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone, ...user.needsSsoUpgrade && !keepsLocal(cookies(c.names.ssoChecked), cookies(c.names.session)) ? { session_upgrade_required: true } : {} };
|
|
446
459
|
}
|
|
447
460
|
async function sessionSummaryWithCookies(config, cookies) {
|
|
448
461
|
const c = resolve(config);
|
|
@@ -453,7 +466,7 @@ async function sessionSummaryWithCookies(config, cookies) {
|
|
|
453
466
|
function readerSsoBootFlags(cookies, names) {
|
|
454
467
|
return {
|
|
455
468
|
hasSession: Boolean(cookies(names.session)),
|
|
456
|
-
suppressed: Boolean(cookies(names.signedOut) || cookies(names.ssoChecked) || cookies(names.pending))
|
|
469
|
+
suppressed: Boolean(cookies(names.signedOut) || cookies(names.ssoChecked) === "1" || cookies(names.pending))
|
|
457
470
|
};
|
|
458
471
|
}
|
|
459
472
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.74",
|
|
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
|
"main": "./dist/index.cjs",
|