@scalemule/news-publication 0.0.85 → 0.0.87
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/article-workflow-client.cjs +12 -2
- package/dist/article-workflow-client.js +12 -2
- package/dist/reader-sso-client.cjs +1 -1
- package/dist/reader-sso-client.js +1 -1
- package/dist/reader-sso.cjs +40 -3
- package/dist/reader-sso.d.cts +2 -0
- package/dist/reader-sso.d.ts +2 -0
- package/dist/reader-sso.js +40 -3
- package/package.json +1 -1
|
@@ -358,6 +358,16 @@ var canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v ===
|
|
|
358
358
|
Object.keys(v).sort().map((k) => [k, canonical2(v[k])])
|
|
359
359
|
) : v;
|
|
360
360
|
var equal = (a, b) => JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
|
|
361
|
+
var uuidIdentity = (value) => {
|
|
362
|
+
if (typeof value !== "string" || !/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)) return value;
|
|
363
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
364
|
+
};
|
|
365
|
+
var comparableDocument = (document) => document && typeof document === "object" ? { ...document, ...Array.isArray(document.media) ? { media: document.media.map((media) => ({
|
|
366
|
+
...media,
|
|
367
|
+
contribution_id: uuidIdentity(media.contribution_id),
|
|
368
|
+
file_id: uuidIdentity(media.file_id)
|
|
369
|
+
})) } : {} } : document;
|
|
370
|
+
var documentsEqual = (a, b) => equal(comparableDocument(a), comparableDocument(b));
|
|
361
371
|
var digest2 = async (v) => [
|
|
362
372
|
...new Uint8Array(
|
|
363
373
|
await crypto.subtle.digest(
|
|
@@ -512,7 +522,7 @@ async function runArticleEdit(t, input) {
|
|
|
512
522
|
state.revisionId
|
|
513
523
|
);
|
|
514
524
|
const selected = currentRevision(record);
|
|
515
|
-
if (!selected || !
|
|
525
|
+
if (!selected || !documentsEqual(selected.document, state.document))
|
|
516
526
|
throw new ArticleEditOperationError(
|
|
517
527
|
"VERIFICATION_CONFLICT",
|
|
518
528
|
stage,
|
|
@@ -580,7 +590,7 @@ async function runArticleEdit(t, input) {
|
|
|
580
590
|
state.savedGeneration = saved.generation;
|
|
581
591
|
await save("verify");
|
|
582
592
|
record = await request(article + "/collaboration");
|
|
583
|
-
if (record.current_revision_id !== state.revisionId || !
|
|
593
|
+
if (record.current_revision_id !== state.revisionId || !documentsEqual(currentRevision(record)?.document, state.document))
|
|
584
594
|
throw new ArticleEditOperationError(
|
|
585
595
|
"ARTICLE_CHANGED",
|
|
586
596
|
stage,
|
|
@@ -356,6 +356,16 @@ var canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v ===
|
|
|
356
356
|
Object.keys(v).sort().map((k) => [k, canonical2(v[k])])
|
|
357
357
|
) : v;
|
|
358
358
|
var equal = (a, b) => JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
|
|
359
|
+
var uuidIdentity = (value) => {
|
|
360
|
+
if (typeof value !== "string" || !/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)) return value;
|
|
361
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
362
|
+
};
|
|
363
|
+
var comparableDocument = (document) => document && typeof document === "object" ? { ...document, ...Array.isArray(document.media) ? { media: document.media.map((media) => ({
|
|
364
|
+
...media,
|
|
365
|
+
contribution_id: uuidIdentity(media.contribution_id),
|
|
366
|
+
file_id: uuidIdentity(media.file_id)
|
|
367
|
+
})) } : {} } : document;
|
|
368
|
+
var documentsEqual = (a, b) => equal(comparableDocument(a), comparableDocument(b));
|
|
359
369
|
var digest2 = async (v) => [
|
|
360
370
|
...new Uint8Array(
|
|
361
371
|
await crypto.subtle.digest(
|
|
@@ -510,7 +520,7 @@ async function runArticleEdit(t, input) {
|
|
|
510
520
|
state.revisionId
|
|
511
521
|
);
|
|
512
522
|
const selected = currentRevision(record);
|
|
513
|
-
if (!selected || !
|
|
523
|
+
if (!selected || !documentsEqual(selected.document, state.document))
|
|
514
524
|
throw new ArticleEditOperationError(
|
|
515
525
|
"VERIFICATION_CONFLICT",
|
|
516
526
|
stage,
|
|
@@ -578,7 +588,7 @@ async function runArticleEdit(t, input) {
|
|
|
578
588
|
state.savedGeneration = saved.generation;
|
|
579
589
|
await save("verify");
|
|
580
590
|
record = await request(article + "/collaboration");
|
|
581
|
-
if (record.current_revision_id !== state.revisionId || !
|
|
591
|
+
if (record.current_revision_id !== state.revisionId || !documentsEqual(currentRevision(record)?.document, state.document))
|
|
582
592
|
throw new ArticleEditOperationError(
|
|
583
593
|
"ARTICLE_CHANGED",
|
|
584
594
|
stage,
|
|
@@ -376,7 +376,7 @@ function AccountLinkForm({
|
|
|
376
376
|
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "password", autoComplete: "current-password", required: true, minLength: 1, maxLength: 1024, value: password, onChange: (e) => setPassword(e.target.value) })
|
|
377
377
|
] }),
|
|
378
378
|
mfaRequired && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
379
|
-
"
|
|
379
|
+
"Verification code",
|
|
380
380
|
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", inputMode: "numeric", autoComplete: "one-time-code", required: true, maxLength: 32, value: code, onChange: (e) => setCode(e.target.value) })
|
|
381
381
|
] }),
|
|
382
382
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: classNames.button, disabled: busy || !password || mfaRequired && !code, children: busy ? "Connecting\u2026" : "Connect and sign in" }),
|
|
@@ -374,7 +374,7 @@ function AccountLinkForm({
|
|
|
374
374
|
/* @__PURE__ */ jsx("input", { type: "password", autoComplete: "current-password", required: true, minLength: 1, maxLength: 1024, value: password, onChange: (e) => setPassword(e.target.value) })
|
|
375
375
|
] }),
|
|
376
376
|
mfaRequired && /* @__PURE__ */ jsxs("label", { children: [
|
|
377
|
-
"
|
|
377
|
+
"Verification code",
|
|
378
378
|
/* @__PURE__ */ jsx("input", { type: "text", inputMode: "numeric", autoComplete: "one-time-code", required: true, maxLength: 32, value: code, onChange: (e) => setCode(e.target.value) })
|
|
379
379
|
] }),
|
|
380
380
|
/* @__PURE__ */ jsx("button", { className: classNames.button, disabled: busy || !password || mfaRequired && !code, children: busy ? "Connecting\u2026" : "Connect and sign in" }),
|
package/dist/reader-sso.cjs
CHANGED
|
@@ -34,7 +34,8 @@ function readerSsoCookieNames(prefix) {
|
|
|
34
34
|
newsletter: `${prefix}-newsletter`,
|
|
35
35
|
ssoChecked: `${prefix}-sso-checked`,
|
|
36
36
|
signedOut: `${prefix}-signed-out`,
|
|
37
|
-
pending: `${prefix}-sso-pending
|
|
37
|
+
pending: `${prefix}-sso-pending`,
|
|
38
|
+
linkCode: `${prefix}-link-code`
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
|
|
@@ -365,6 +366,35 @@ function linkFailureMessage(code) {
|
|
|
365
366
|
return null;
|
|
366
367
|
}
|
|
367
368
|
}
|
|
369
|
+
function linkEmailCodeFailure(code, response, cookieName) {
|
|
370
|
+
switch (code) {
|
|
371
|
+
case "LOGIN_CHALLENGE_REQUIRED": {
|
|
372
|
+
const body = response.body;
|
|
373
|
+
let token = "";
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(typeof body?.error?.message === "string" ? body.error.message : "{}");
|
|
376
|
+
token = typeof parsed.challenge_token === "string" && parsed.challenge_token.length <= 256 ? parsed.challenge_token : "";
|
|
377
|
+
} catch {
|
|
378
|
+
token = "";
|
|
379
|
+
}
|
|
380
|
+
if (!token) return null;
|
|
381
|
+
return {
|
|
382
|
+
status: 401,
|
|
383
|
+
mfaRequired: true,
|
|
384
|
+
error: "We emailed a 6-digit code to your reader account's address. Enter it to finish connecting your sign-in.",
|
|
385
|
+
cookies: [{ name: cookieName, value: token, maxAge: 600 }]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
case "LOGIN_CHALLENGE_INVALID":
|
|
389
|
+
return { status: 401, mfaRequired: true, error: "That code is incorrect or expired. Check the most recent email, or start over for a new code.", cookies: [] };
|
|
390
|
+
case "CHALLENGE_RATE_LIMITED":
|
|
391
|
+
return { status: 429, mfaRequired: true, error: "Please wait a minute before asking for another code.", cookies: [] };
|
|
392
|
+
case "LOGIN_CHALLENGE_LOCKED":
|
|
393
|
+
return { status: 429, error: "Too many incorrect codes. Reset your reader account password to continue.", cookies: [{ name: cookieName, value: "", maxAge: 0 }] };
|
|
394
|
+
default:
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
368
398
|
async function linkAccount(config, input) {
|
|
369
399
|
const c = resolve(config);
|
|
370
400
|
const challengeId = typeof input.challenge_id === "string" && CHALLENGE_ID.test(input.challenge_id) ? input.challenge_id : "";
|
|
@@ -378,9 +408,16 @@ async function linkAccount(config, input) {
|
|
|
378
408
|
if (mfaCode && (mfaCode.length > 32 || !/^[A-Za-z0-9-]+$/.test(mfaCode))) {
|
|
379
409
|
return { ok: false, status: 400, error: "Enter the code exactly as your authenticator app shows it.", cookies: [], mfaRequired: true };
|
|
380
410
|
}
|
|
411
|
+
const emailChallenge = input.cookies(c.names.linkCode) || "";
|
|
412
|
+
const linkBody = emailChallenge && mfaCode ? { challenge_id: challengeId, password, challenge_token: emailChallenge, challenge_code: mfaCode } : { challenge_id: challengeId, password, ...mfaCode ? { mfa_code: mfaCode } : {} };
|
|
381
413
|
try {
|
|
382
|
-
const response = await c.request("/v1/auth/oauth/ledvery/link", { method: "POST", body:
|
|
414
|
+
const response = await c.request("/v1/auth/oauth/ledvery/link", { method: "POST", body: linkBody });
|
|
383
415
|
if (failed(response)) {
|
|
416
|
+
const emailCode = linkEmailCodeFailure(errorCode(response), response, c.names.linkCode);
|
|
417
|
+
if (emailCode) {
|
|
418
|
+
emit(c, { name: "reader_signin_failed", properties: { reason: "mfa_required", mode: "interactive" } });
|
|
419
|
+
return { ok: false, ...emailCode };
|
|
420
|
+
}
|
|
384
421
|
const specific = linkFailureMessage(errorCode(response));
|
|
385
422
|
if (specific) {
|
|
386
423
|
emit(c, { name: "reader_signin_failed", properties: { reason: specific.mfaRequired ? "mfa_required" : "link_invalid", mode: "interactive" } });
|
|
@@ -391,7 +428,7 @@ async function linkAccount(config, input) {
|
|
|
391
428
|
}
|
|
392
429
|
const session = issuedSession(response, c.now());
|
|
393
430
|
const flow = { digest: "", subscribe: input.subscribe === true, expires: 0, returnTo: next, mode: "interactive", provider: void 0 };
|
|
394
|
-
const result = await establish(c, { session, flow, method: "link", cookies: [], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
|
|
431
|
+
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 } });
|
|
395
432
|
if (result.kind === "signed-in") emit(c, { name: "reader_account_linked", properties: {} });
|
|
396
433
|
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 };
|
|
397
434
|
} catch {
|
package/dist/reader-sso.d.cts
CHANGED
|
@@ -23,6 +23,8 @@ type ReaderSsoCookieNames = {
|
|
|
23
23
|
signedOut: string;
|
|
24
24
|
/** A verified session waiting for the reader to choose between two accounts. */
|
|
25
25
|
pending: string;
|
|
26
|
+
/** Linking onto an unconfirmed account: the platform's emailed-code challenge token (10 minutes). */
|
|
27
|
+
linkCode: string;
|
|
26
28
|
};
|
|
27
29
|
/** Cookie names derived from a `__Host-` prefix, e.g. `__Host-media` → `__Host-media-reader`. */
|
|
28
30
|
declare function readerSsoCookieNames(prefix: string): ReaderSsoCookieNames;
|
package/dist/reader-sso.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ type ReaderSsoCookieNames = {
|
|
|
23
23
|
signedOut: string;
|
|
24
24
|
/** A verified session waiting for the reader to choose between two accounts. */
|
|
25
25
|
pending: string;
|
|
26
|
+
/** Linking onto an unconfirmed account: the platform's emailed-code challenge token (10 minutes). */
|
|
27
|
+
linkCode: string;
|
|
26
28
|
};
|
|
27
29
|
/** Cookie names derived from a `__Host-` prefix, e.g. `__Host-media` → `__Host-media-reader`. */
|
|
28
30
|
declare function readerSsoCookieNames(prefix: string): ReaderSsoCookieNames;
|
package/dist/reader-sso.js
CHANGED
|
@@ -19,7 +19,8 @@ function readerSsoCookieNames(prefix) {
|
|
|
19
19
|
newsletter: `${prefix}-newsletter`,
|
|
20
20
|
ssoChecked: `${prefix}-sso-checked`,
|
|
21
21
|
signedOut: `${prefix}-signed-out`,
|
|
22
|
-
pending: `${prefix}-sso-pending
|
|
22
|
+
pending: `${prefix}-sso-pending`,
|
|
23
|
+
linkCode: `${prefix}-link-code`
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
var readerSsoCookieOptions = { httpOnly: true, secure: true, sameSite: "lax", path: "/" };
|
|
@@ -350,6 +351,35 @@ function linkFailureMessage(code) {
|
|
|
350
351
|
return null;
|
|
351
352
|
}
|
|
352
353
|
}
|
|
354
|
+
function linkEmailCodeFailure(code, response, cookieName) {
|
|
355
|
+
switch (code) {
|
|
356
|
+
case "LOGIN_CHALLENGE_REQUIRED": {
|
|
357
|
+
const body = response.body;
|
|
358
|
+
let token = "";
|
|
359
|
+
try {
|
|
360
|
+
const parsed = JSON.parse(typeof body?.error?.message === "string" ? body.error.message : "{}");
|
|
361
|
+
token = typeof parsed.challenge_token === "string" && parsed.challenge_token.length <= 256 ? parsed.challenge_token : "";
|
|
362
|
+
} catch {
|
|
363
|
+
token = "";
|
|
364
|
+
}
|
|
365
|
+
if (!token) return null;
|
|
366
|
+
return {
|
|
367
|
+
status: 401,
|
|
368
|
+
mfaRequired: true,
|
|
369
|
+
error: "We emailed a 6-digit code to your reader account's address. Enter it to finish connecting your sign-in.",
|
|
370
|
+
cookies: [{ name: cookieName, value: token, maxAge: 600 }]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
case "LOGIN_CHALLENGE_INVALID":
|
|
374
|
+
return { status: 401, mfaRequired: true, error: "That code is incorrect or expired. Check the most recent email, or start over for a new code.", cookies: [] };
|
|
375
|
+
case "CHALLENGE_RATE_LIMITED":
|
|
376
|
+
return { status: 429, mfaRequired: true, error: "Please wait a minute before asking for another code.", cookies: [] };
|
|
377
|
+
case "LOGIN_CHALLENGE_LOCKED":
|
|
378
|
+
return { status: 429, error: "Too many incorrect codes. Reset your reader account password to continue.", cookies: [{ name: cookieName, value: "", maxAge: 0 }] };
|
|
379
|
+
default:
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
353
383
|
async function linkAccount(config, input) {
|
|
354
384
|
const c = resolve(config);
|
|
355
385
|
const challengeId = typeof input.challenge_id === "string" && CHALLENGE_ID.test(input.challenge_id) ? input.challenge_id : "";
|
|
@@ -363,9 +393,16 @@ async function linkAccount(config, input) {
|
|
|
363
393
|
if (mfaCode && (mfaCode.length > 32 || !/^[A-Za-z0-9-]+$/.test(mfaCode))) {
|
|
364
394
|
return { ok: false, status: 400, error: "Enter the code exactly as your authenticator app shows it.", cookies: [], mfaRequired: true };
|
|
365
395
|
}
|
|
396
|
+
const emailChallenge = input.cookies(c.names.linkCode) || "";
|
|
397
|
+
const linkBody = emailChallenge && mfaCode ? { challenge_id: challengeId, password, challenge_token: emailChallenge, challenge_code: mfaCode } : { challenge_id: challengeId, password, ...mfaCode ? { mfa_code: mfaCode } : {} };
|
|
366
398
|
try {
|
|
367
|
-
const response = await c.request("/v1/auth/oauth/ledvery/link", { method: "POST", body:
|
|
399
|
+
const response = await c.request("/v1/auth/oauth/ledvery/link", { method: "POST", body: linkBody });
|
|
368
400
|
if (failed(response)) {
|
|
401
|
+
const emailCode = linkEmailCodeFailure(errorCode(response), response, c.names.linkCode);
|
|
402
|
+
if (emailCode) {
|
|
403
|
+
emit(c, { name: "reader_signin_failed", properties: { reason: "mfa_required", mode: "interactive" } });
|
|
404
|
+
return { ok: false, ...emailCode };
|
|
405
|
+
}
|
|
369
406
|
const specific = linkFailureMessage(errorCode(response));
|
|
370
407
|
if (specific) {
|
|
371
408
|
emit(c, { name: "reader_signin_failed", properties: { reason: specific.mfaRequired ? "mfa_required" : "link_invalid", mode: "interactive" } });
|
|
@@ -376,7 +413,7 @@ async function linkAccount(config, input) {
|
|
|
376
413
|
}
|
|
377
414
|
const session = issuedSession(response, c.now());
|
|
378
415
|
const flow = { digest: "", subscribe: input.subscribe === true, expires: 0, returnTo: next, mode: "interactive", provider: void 0 };
|
|
379
|
-
const result = await establish(c, { session, flow, method: "link", cookies: [], input, negativeCache: { name: c.names.ssoChecked, value: "1", maxAge: READER_SSO_NEGATIVE_CACHE_SECONDS } });
|
|
416
|
+
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 } });
|
|
380
417
|
if (result.kind === "signed-in") emit(c, { name: "reader_account_linked", properties: {} });
|
|
381
418
|
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 };
|
|
382
419
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.87",
|
|
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": {
|