@scalemule/news-publication 0.0.75 → 0.0.77
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 +30 -0
- package/bin/scalemule-article.mjs +83 -0
- package/dist/article-workflow-client.cjs +81 -0
- package/dist/article-workflow-client.d.cts +29 -1
- package/dist/article-workflow-client.d.ts +29 -1
- package/dist/article-workflow-client.js +81 -0
- package/dist/reader-sso-client.cjs +55 -7
- package/dist/reader-sso-client.d.cts +4 -1
- package/dist/reader-sso-client.d.ts +4 -1
- package/dist/reader-sso-client.js +53 -8
- package/dist/reader-sso.cjs +4 -4
- package/dist/reader-sso.js +4 -4
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -337,3 +337,33 @@ explicit commands; acceptance does not certify scan, image or audio completion.
|
|
|
337
337
|
The platform contract and a complete example live in
|
|
338
338
|
`docs/news-platform/ARTICLE-WORKFLOW-V1.md` in the ScaleMule meta-repo.
|
|
339
339
|
Do not use direct SQL, S3 paths, base64 bodies or invented media readiness.
|
|
340
|
+
|
|
341
|
+
### Replace an existing, prepared cover
|
|
342
|
+
|
|
343
|
+
For a legacy published article and an already public, scanned Photo asset,
|
|
344
|
+
use `client.replacePublishedCover(...)` or the included executable:
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
scalemule-article inspect --publication "$PUBLICATION_ID" --article "$ARTICLE_ID"
|
|
348
|
+
scalemule-article replace-cover --publication "$PUBLICATION_ID" --article "$ARTICLE_ID" \
|
|
349
|
+
--file-id "$PHOTO_FILE_ID" --expected-version "$VERSION" --metadata photo.json
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Use normal `SCALEMULE_API_KEY` and `SCALEMULE_ACCESS_TOKEN` authorization.
|
|
353
|
+
`inspect` reports the public-projection version and whether collaboration is
|
|
354
|
+
enabled, without logging draft text or review capabilities. The metadata file
|
|
355
|
+
records caption, credit, factual alt text, `rightsConfirmed`, `aiGenerated` and
|
|
356
|
+
the internal provenance `label` (null for an ordinary photograph).
|
|
357
|
+
|
|
358
|
+
The replacement reads measured hero/social manifests and makes one version-fenced
|
|
359
|
+
media write. A missing social rendition gets one normal Photo transform request
|
|
360
|
+
and a fresh measured metadata lookup; pending work stops without polling forever.
|
|
361
|
+
It never invents dimensions or delivery URLs, uploads another copy,
|
|
362
|
+
restarts narration, republishes text or silently enrolls a story. Missing or
|
|
363
|
+
incomplete Photo renditions leave the existing cover unchanged; inspect and
|
|
364
|
+
resume with the same asset. Collaboration-managed stories retain their exact
|
|
365
|
+
revision approval/release requirements and the server rejects this legacy path.
|
|
366
|
+
This helper does not source or ingest a new photograph. Use managed asset intake
|
|
367
|
+
for that phase, preserving source rights and private capture information.
|
|
368
|
+
Reported elapsed time covers the API operation; verify the real public page and
|
|
369
|
+
homepage allocation separately before claiming the replacement is visible.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { PublicationArticleClient } from '../dist/article-workflow-client.js';
|
|
5
|
+
|
|
6
|
+
const help = `Usage:
|
|
7
|
+
scalemule-article inspect --publication ID --article ID
|
|
8
|
+
scalemule-article replace-cover --publication ID --article ID --file-id ID --expected-version N --metadata photo.json
|
|
9
|
+
|
|
10
|
+
Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized member).
|
|
11
|
+
Platform admins may use their normal access token with --application-id ID.
|
|
12
|
+
Optional --api-url URL defaults to https://api.scalemule.com.
|
|
13
|
+
|
|
14
|
+
replace-cover selects an already public, scanned Photo asset. It reads measured
|
|
15
|
+
hero and social renditions, preserves the article text, and changes media with
|
|
16
|
+
the supplied version fence. It never uploads raw bytes, republishes the story,
|
|
17
|
+
starts narration, or silently enrolls a legacy story into collaboration.
|
|
18
|
+
Collaboration-managed articles must use their immutable review/release workflow.
|
|
19
|
+
|
|
20
|
+
photo.json: { "caption": "...", "credit": "...", "alt": "...",
|
|
21
|
+
"rightsConfirmed": true, "aiGenerated": false, "label": null }
|
|
22
|
+
Optional metadata: focalPoint, socialRenditionKey (a Photo metadata lookup key).
|
|
23
|
+
The label is internal provenance, subject to each publication's caption policy.
|
|
24
|
+
Pending scan/renditions stop without changing the existing cover. Inspect the
|
|
25
|
+
same asset instead of uploading again. Output records API timing, not a claim
|
|
26
|
+
that every website/CDN cache already displays the new image.
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const { values, positionals } = parseArgs({
|
|
31
|
+
allowPositionals: true, strict: true,
|
|
32
|
+
options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url'].map(k => [k, { type: 'string' }]).concat([['help', { type: 'boolean' }]])),
|
|
33
|
+
});
|
|
34
|
+
if (values.help || !positionals.length) {
|
|
35
|
+
console.log(help);
|
|
36
|
+
} else {
|
|
37
|
+
const [command, ...extra] = positionals;
|
|
38
|
+
if (extra.length || !['inspect', 'replace-cover'].includes(command)) throw Error('Use inspect or replace-cover; see --help');
|
|
39
|
+
for (const key of ['publication', 'article']) if (!values[key]) throw Error(`Missing --${key}`);
|
|
40
|
+
if (!process.env.SCALEMULE_ACCESS_TOKEN) throw Error('SCALEMULE_ACCESS_TOKEN is required; use normal member or platform admin authorization');
|
|
41
|
+
if (!values['application-id'] && !process.env.SCALEMULE_API_KEY) throw Error('Member calls also require SCALEMULE_API_KEY');
|
|
42
|
+
const client = new PublicationArticleClient({
|
|
43
|
+
apiUrl: values['api-url'] ?? 'https://api.scalemule.com',
|
|
44
|
+
publicationId: values.publication,
|
|
45
|
+
applicationId: values['application-id'],
|
|
46
|
+
apiKey: process.env.SCALEMULE_API_KEY,
|
|
47
|
+
accessToken: process.env.SCALEMULE_ACCESS_TOKEN,
|
|
48
|
+
});
|
|
49
|
+
const started = performance.now();
|
|
50
|
+
if (command === 'inspect') {
|
|
51
|
+
const [options, processing, collaboration] = await Promise.all([
|
|
52
|
+
client.options(values.article), client.processing(values.article),
|
|
53
|
+
client.get(values.article).then(current => ({ enabled: true, generation: current.generation, status: current.status })).catch(error => {
|
|
54
|
+
if (error.status === 404 && error.message === 'Story collaboration is not enabled') return { enabled: false };
|
|
55
|
+
throw error;
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
console.log(JSON.stringify({
|
|
59
|
+
article_id: processing.article_id, version: processing.version,
|
|
60
|
+
publication_status: processing.publication_status,
|
|
61
|
+
workflow_version: options.workflow_version, profile_version: options.profile_version,
|
|
62
|
+
capabilities: options.capabilities, narration: processing.narration,
|
|
63
|
+
assets: processing.assets, collaboration, elapsed_ms: Math.round(performance.now() - started),
|
|
64
|
+
}, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
for (const key of ['file-id', 'expected-version', 'metadata']) if (!values[key]) throw Error(`Missing --${key}`);
|
|
67
|
+
const metadata = JSON.parse(await readFile(values.metadata, 'utf8'));
|
|
68
|
+
for (const key of ['caption', 'credit', 'alt']) if (typeof metadata[key] !== 'string') throw Error(`photo.json requires a string ${key}`);
|
|
69
|
+
if (typeof metadata.aiGenerated !== 'boolean' || !(metadata.label === null || typeof metadata.label === 'string')) throw Error('photo.json must explicitly record aiGenerated and label');
|
|
70
|
+
const result = await client.replacePublishedCover({
|
|
71
|
+
articleId: values.article, expectedVersion: Number(values['expected-version']),
|
|
72
|
+
fileId: values['file-id'], caption: metadata.caption, credit: metadata.credit,
|
|
73
|
+
alt: metadata.alt, rightsConfirmed: metadata.rightsConfirmed,
|
|
74
|
+
aiGenerated: metadata.aiGenerated, label: metadata.label,
|
|
75
|
+
focalPoint: metadata.focalPoint, socialRenditionKey: metadata.socialRenditionKey,
|
|
76
|
+
});
|
|
77
|
+
console.log(JSON.stringify({ article_id: result.post_id, version: result.version, state: 'cover_updated', elapsed_ms: Math.round(performance.now() - started) }, null, 2));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(JSON.stringify({ error: error.message, status: error.status }));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
@@ -77,6 +77,87 @@ var PublicationArticleClient = class {
|
|
|
77
77
|
`/articles/${encodeURIComponent(articleId)}/processing`
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
|
+
/** Replace a legacy publication cover using ready, measured public Photo renditions.
|
|
81
|
+
* This never enrolls a story, publishes text, creates narration or retries a conflict.
|
|
82
|
+
* Collaboration-managed stories retain their server-enforced immutable revision workflow.
|
|
83
|
+
*/
|
|
84
|
+
async replacePublishedCover(input) {
|
|
85
|
+
if (!Number.isSafeInteger(input.expectedVersion) || input.expectedVersion < 1)
|
|
86
|
+
throw new Error("Use the current public-projection version from processing()");
|
|
87
|
+
if (input.rightsConfirmed !== true || !input.alt.trim())
|
|
88
|
+
throw new Error("Confirm publication rights and supply factual image alt text");
|
|
89
|
+
if (input.aiGenerated && !input.label?.trim())
|
|
90
|
+
throw new Error("Generated art requires its internal provenance label");
|
|
91
|
+
const focal = input.focalPoint ?? { x: 0.5, y: 0.5 };
|
|
92
|
+
if (![focal.x, focal.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
|
|
93
|
+
throw new Error("Focal point must be within the image");
|
|
94
|
+
const photo = `/v1/photos/public/${encodeURIComponent(input.fileId)}/manifest`;
|
|
95
|
+
const socialKey = input.socialRenditionKey ?? "w1200_h630_cover_q85_jpeg";
|
|
96
|
+
const socialPath = `${photo}?rendition=${encodeURIComponent(socialKey)}`;
|
|
97
|
+
const [current, hero, initialSocial] = await Promise.all([
|
|
98
|
+
this.processing(input.articleId),
|
|
99
|
+
this.request(`${photo}?preset=hero`),
|
|
100
|
+
this.request(socialPath)
|
|
101
|
+
]);
|
|
102
|
+
if (current.publication_status !== "published" || current.version !== input.expectedVersion)
|
|
103
|
+
throw new ArticleWorkflowError("Article is not published or its version changed; reload before updating its cover", 409, input.articleId);
|
|
104
|
+
let social = initialSocial;
|
|
105
|
+
if (hero.ready && [hero.file_id, hero.photo_id].includes(input.fileId) && !social.ready && social.file_id === hero.file_id) {
|
|
106
|
+
const spec = /^w(\d+)_h(\d+)_(cover|contain|fill|scaledown)_q(\d+)_(jpeg|webp|avif)$/.exec(socialKey);
|
|
107
|
+
if (spec && Number(spec[1]) > 0 && Number(spec[1]) <= 12e3 && Number(spec[2]) > 0 && Number(spec[2]) <= 12e3 && Number(spec[4]) > 0 && Number(spec[4]) <= 100) {
|
|
108
|
+
const query = new URLSearchParams({ width: spec[1], height: spec[2], fit: spec[3], quality: spec[4], format: spec[5] });
|
|
109
|
+
const url = new URL(this.config.apiUrl.replace(/\/$/, "") + `/v1/photos/public/${encodeURIComponent(input.fileId)}/transform?${query}`);
|
|
110
|
+
const response = await (this.config.fetch ?? fetch)(url, { method: "GET", redirect: "manual", cache: "no-store", signal: AbortSignal.timeout(3e4) });
|
|
111
|
+
const accepted = response.ok && response.headers.get("content-type")?.startsWith("image/") || [302, 303, 307, 308].includes(response.status);
|
|
112
|
+
await response.body?.cancel();
|
|
113
|
+
if (accepted) social = await this.request(socialPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const dimension = (v) => Number.isInteger(v) && Number(v) > 0 && Number(v) <= 12e3;
|
|
117
|
+
const publicUrl = (raw) => {
|
|
118
|
+
try {
|
|
119
|
+
const url = new URL(raw);
|
|
120
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const variants = Object.values(hero.variant_metadata ?? {});
|
|
126
|
+
if (!hero.ready || !social.ready || !hero.file_id || ![hero.file_id, hero.photo_id].includes(input.fileId) || social.file_id !== hero.file_id || !variants.length || variants.length > 20 || variants.some((v) => !dimension(v.width) || !dimension(v.height) || !publicUrl(v.url) || !["jpeg", "webp", "avif"].includes(v.format)) || !dimension(social.width) || !dimension(social.height) || !social.url || !publicUrl(social.url))
|
|
127
|
+
throw new ArticleWorkflowError("Public Photo renditions are not ready with measured dimensions; inspect Photo processing, keep the existing cover and resume with the same asset", 409, input.articleId);
|
|
128
|
+
const ordered = [...variants].sort((a, b) => a.width - b.width);
|
|
129
|
+
const master = [...ordered].reverse().find((v) => v.width <= 1600) ?? ordered[0];
|
|
130
|
+
return this.request(
|
|
131
|
+
`/articles/${encodeURIComponent(input.articleId)}/media`,
|
|
132
|
+
"PUT",
|
|
133
|
+
{
|
|
134
|
+
expected_version: input.expectedVersion,
|
|
135
|
+
cover_image_url: master.url,
|
|
136
|
+
og_image_url: social.url,
|
|
137
|
+
cover_image: {
|
|
138
|
+
file_id: hero.file_id,
|
|
139
|
+
master_url: master.url,
|
|
140
|
+
width: master.width,
|
|
141
|
+
height: master.height,
|
|
142
|
+
aspect_ratio: `${master.width}:${master.height}`,
|
|
143
|
+
alt: input.alt,
|
|
144
|
+
caption: input.caption,
|
|
145
|
+
attribution: input.credit,
|
|
146
|
+
ai_generated: input.aiGenerated,
|
|
147
|
+
label: input.label,
|
|
148
|
+
focal_point: focal,
|
|
149
|
+
variants
|
|
150
|
+
},
|
|
151
|
+
open_graph_image: {
|
|
152
|
+
file_id: hero.file_id,
|
|
153
|
+
url: social.url,
|
|
154
|
+
width: social.width,
|
|
155
|
+
height: social.height,
|
|
156
|
+
aspect_ratio: `${social.width}:${social.height}`
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
80
161
|
/** Existing private narration path for a saved revision and a before-publication audio policy. */
|
|
81
162
|
narrateRevision(articleId, expectedGeneration) {
|
|
82
163
|
return this.request(
|
|
@@ -33,6 +33,24 @@ type AssetMetadata = {
|
|
|
33
33
|
rights_confirmed: boolean;
|
|
34
34
|
private_preview_only?: boolean;
|
|
35
35
|
};
|
|
36
|
+
type PublishedCoverReplacement = {
|
|
37
|
+
articleId: string;
|
|
38
|
+
expectedVersion: number;
|
|
39
|
+
/** A real, already public Photo/Storage ID, never an invented ID or URL. */
|
|
40
|
+
fileId: string;
|
|
41
|
+
rightsConfirmed: boolean;
|
|
42
|
+
caption: string;
|
|
43
|
+
credit: string;
|
|
44
|
+
alt: string;
|
|
45
|
+
aiGenerated: boolean;
|
|
46
|
+
label: string | null;
|
|
47
|
+
focalPoint?: {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
};
|
|
51
|
+
/** Metadata lookup key only; no rendition URL or dimensions are inferred. */
|
|
52
|
+
socialRenditionKey?: string;
|
|
53
|
+
};
|
|
36
54
|
type ArticleProcessing = {
|
|
37
55
|
article_id: string;
|
|
38
56
|
version: number;
|
|
@@ -145,6 +163,16 @@ declare class PublicationArticleClient {
|
|
|
145
163
|
}>;
|
|
146
164
|
get(articleId: string): Promise<Record>;
|
|
147
165
|
processing(articleId: string): Promise<ArticleProcessing>;
|
|
166
|
+
/** Replace a legacy publication cover using ready, measured public Photo renditions.
|
|
167
|
+
* This never enrolls a story, publishes text, creates narration or retries a conflict.
|
|
168
|
+
* Collaboration-managed stories retain their server-enforced immutable revision workflow.
|
|
169
|
+
*/
|
|
170
|
+
replacePublishedCover(input: PublishedCoverReplacement): Promise<{
|
|
171
|
+
post_id: string;
|
|
172
|
+
version: number;
|
|
173
|
+
cover_image_url: string;
|
|
174
|
+
og_image_url: string;
|
|
175
|
+
}>;
|
|
148
176
|
/** Existing private narration path for a saved revision and a before-publication audio policy. */
|
|
149
177
|
narrateRevision(articleId: string, expectedGeneration: number): Promise<{
|
|
150
178
|
status: string;
|
|
@@ -185,4 +213,4 @@ declare class PublicationArticleClient {
|
|
|
185
213
|
}): Promise<ArticleOperation>;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
export { type ArticleDocument, type ArticleMedia, type ArticleOperation, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, PublicationArticleClient };
|
|
216
|
+
export { type ArticleDocument, type ArticleMedia, type ArticleOperation, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, PublicationArticleClient, type PublishedCoverReplacement };
|
|
@@ -33,6 +33,24 @@ type AssetMetadata = {
|
|
|
33
33
|
rights_confirmed: boolean;
|
|
34
34
|
private_preview_only?: boolean;
|
|
35
35
|
};
|
|
36
|
+
type PublishedCoverReplacement = {
|
|
37
|
+
articleId: string;
|
|
38
|
+
expectedVersion: number;
|
|
39
|
+
/** A real, already public Photo/Storage ID, never an invented ID or URL. */
|
|
40
|
+
fileId: string;
|
|
41
|
+
rightsConfirmed: boolean;
|
|
42
|
+
caption: string;
|
|
43
|
+
credit: string;
|
|
44
|
+
alt: string;
|
|
45
|
+
aiGenerated: boolean;
|
|
46
|
+
label: string | null;
|
|
47
|
+
focalPoint?: {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
};
|
|
51
|
+
/** Metadata lookup key only; no rendition URL or dimensions are inferred. */
|
|
52
|
+
socialRenditionKey?: string;
|
|
53
|
+
};
|
|
36
54
|
type ArticleProcessing = {
|
|
37
55
|
article_id: string;
|
|
38
56
|
version: number;
|
|
@@ -145,6 +163,16 @@ declare class PublicationArticleClient {
|
|
|
145
163
|
}>;
|
|
146
164
|
get(articleId: string): Promise<Record>;
|
|
147
165
|
processing(articleId: string): Promise<ArticleProcessing>;
|
|
166
|
+
/** Replace a legacy publication cover using ready, measured public Photo renditions.
|
|
167
|
+
* This never enrolls a story, publishes text, creates narration or retries a conflict.
|
|
168
|
+
* Collaboration-managed stories retain their server-enforced immutable revision workflow.
|
|
169
|
+
*/
|
|
170
|
+
replacePublishedCover(input: PublishedCoverReplacement): Promise<{
|
|
171
|
+
post_id: string;
|
|
172
|
+
version: number;
|
|
173
|
+
cover_image_url: string;
|
|
174
|
+
og_image_url: string;
|
|
175
|
+
}>;
|
|
148
176
|
/** Existing private narration path for a saved revision and a before-publication audio policy. */
|
|
149
177
|
narrateRevision(articleId: string, expectedGeneration: number): Promise<{
|
|
150
178
|
status: string;
|
|
@@ -185,4 +213,4 @@ declare class PublicationArticleClient {
|
|
|
185
213
|
}): Promise<ArticleOperation>;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
export { type ArticleDocument, type ArticleMedia, type ArticleOperation, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, PublicationArticleClient };
|
|
216
|
+
export { type ArticleDocument, type ArticleMedia, type ArticleOperation, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, PublicationArticleClient, type PublishedCoverReplacement };
|
|
@@ -75,6 +75,87 @@ var PublicationArticleClient = class {
|
|
|
75
75
|
`/articles/${encodeURIComponent(articleId)}/processing`
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
|
+
/** Replace a legacy publication cover using ready, measured public Photo renditions.
|
|
79
|
+
* This never enrolls a story, publishes text, creates narration or retries a conflict.
|
|
80
|
+
* Collaboration-managed stories retain their server-enforced immutable revision workflow.
|
|
81
|
+
*/
|
|
82
|
+
async replacePublishedCover(input) {
|
|
83
|
+
if (!Number.isSafeInteger(input.expectedVersion) || input.expectedVersion < 1)
|
|
84
|
+
throw new Error("Use the current public-projection version from processing()");
|
|
85
|
+
if (input.rightsConfirmed !== true || !input.alt.trim())
|
|
86
|
+
throw new Error("Confirm publication rights and supply factual image alt text");
|
|
87
|
+
if (input.aiGenerated && !input.label?.trim())
|
|
88
|
+
throw new Error("Generated art requires its internal provenance label");
|
|
89
|
+
const focal = input.focalPoint ?? { x: 0.5, y: 0.5 };
|
|
90
|
+
if (![focal.x, focal.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
|
|
91
|
+
throw new Error("Focal point must be within the image");
|
|
92
|
+
const photo = `/v1/photos/public/${encodeURIComponent(input.fileId)}/manifest`;
|
|
93
|
+
const socialKey = input.socialRenditionKey ?? "w1200_h630_cover_q85_jpeg";
|
|
94
|
+
const socialPath = `${photo}?rendition=${encodeURIComponent(socialKey)}`;
|
|
95
|
+
const [current, hero, initialSocial] = await Promise.all([
|
|
96
|
+
this.processing(input.articleId),
|
|
97
|
+
this.request(`${photo}?preset=hero`),
|
|
98
|
+
this.request(socialPath)
|
|
99
|
+
]);
|
|
100
|
+
if (current.publication_status !== "published" || current.version !== input.expectedVersion)
|
|
101
|
+
throw new ArticleWorkflowError("Article is not published or its version changed; reload before updating its cover", 409, input.articleId);
|
|
102
|
+
let social = initialSocial;
|
|
103
|
+
if (hero.ready && [hero.file_id, hero.photo_id].includes(input.fileId) && !social.ready && social.file_id === hero.file_id) {
|
|
104
|
+
const spec = /^w(\d+)_h(\d+)_(cover|contain|fill|scaledown)_q(\d+)_(jpeg|webp|avif)$/.exec(socialKey);
|
|
105
|
+
if (spec && Number(spec[1]) > 0 && Number(spec[1]) <= 12e3 && Number(spec[2]) > 0 && Number(spec[2]) <= 12e3 && Number(spec[4]) > 0 && Number(spec[4]) <= 100) {
|
|
106
|
+
const query = new URLSearchParams({ width: spec[1], height: spec[2], fit: spec[3], quality: spec[4], format: spec[5] });
|
|
107
|
+
const url = new URL(this.config.apiUrl.replace(/\/$/, "") + `/v1/photos/public/${encodeURIComponent(input.fileId)}/transform?${query}`);
|
|
108
|
+
const response = await (this.config.fetch ?? fetch)(url, { method: "GET", redirect: "manual", cache: "no-store", signal: AbortSignal.timeout(3e4) });
|
|
109
|
+
const accepted = response.ok && response.headers.get("content-type")?.startsWith("image/") || [302, 303, 307, 308].includes(response.status);
|
|
110
|
+
await response.body?.cancel();
|
|
111
|
+
if (accepted) social = await this.request(socialPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const dimension = (v) => Number.isInteger(v) && Number(v) > 0 && Number(v) <= 12e3;
|
|
115
|
+
const publicUrl = (raw) => {
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(raw);
|
|
118
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const variants = Object.values(hero.variant_metadata ?? {});
|
|
124
|
+
if (!hero.ready || !social.ready || !hero.file_id || ![hero.file_id, hero.photo_id].includes(input.fileId) || social.file_id !== hero.file_id || !variants.length || variants.length > 20 || variants.some((v) => !dimension(v.width) || !dimension(v.height) || !publicUrl(v.url) || !["jpeg", "webp", "avif"].includes(v.format)) || !dimension(social.width) || !dimension(social.height) || !social.url || !publicUrl(social.url))
|
|
125
|
+
throw new ArticleWorkflowError("Public Photo renditions are not ready with measured dimensions; inspect Photo processing, keep the existing cover and resume with the same asset", 409, input.articleId);
|
|
126
|
+
const ordered = [...variants].sort((a, b) => a.width - b.width);
|
|
127
|
+
const master = [...ordered].reverse().find((v) => v.width <= 1600) ?? ordered[0];
|
|
128
|
+
return this.request(
|
|
129
|
+
`/articles/${encodeURIComponent(input.articleId)}/media`,
|
|
130
|
+
"PUT",
|
|
131
|
+
{
|
|
132
|
+
expected_version: input.expectedVersion,
|
|
133
|
+
cover_image_url: master.url,
|
|
134
|
+
og_image_url: social.url,
|
|
135
|
+
cover_image: {
|
|
136
|
+
file_id: hero.file_id,
|
|
137
|
+
master_url: master.url,
|
|
138
|
+
width: master.width,
|
|
139
|
+
height: master.height,
|
|
140
|
+
aspect_ratio: `${master.width}:${master.height}`,
|
|
141
|
+
alt: input.alt,
|
|
142
|
+
caption: input.caption,
|
|
143
|
+
attribution: input.credit,
|
|
144
|
+
ai_generated: input.aiGenerated,
|
|
145
|
+
label: input.label,
|
|
146
|
+
focal_point: focal,
|
|
147
|
+
variants
|
|
148
|
+
},
|
|
149
|
+
open_graph_image: {
|
|
150
|
+
file_id: hero.file_id,
|
|
151
|
+
url: social.url,
|
|
152
|
+
width: social.width,
|
|
153
|
+
height: social.height,
|
|
154
|
+
aspect_ratio: `${social.width}:${social.height}`
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
78
159
|
/** Existing private narration path for a saved revision and a before-publication audio policy. */
|
|
79
160
|
narrateRevision(articleId, expectedGeneration) {
|
|
80
161
|
return this.request(
|
|
@@ -150,19 +150,64 @@ function greetingText(profile, now = /* @__PURE__ */ new Date(), fallback = "Wel
|
|
|
150
150
|
if (!name) return fallback;
|
|
151
151
|
return `${greetingWord(localHour(now, profile.time_zone, profile.locale))}, ${name}`;
|
|
152
152
|
}
|
|
153
|
+
var READER_GREETING_POLL_MS = 3e4;
|
|
154
|
+
var READER_GREETING_TIMEOUT_MS = 8e3;
|
|
155
|
+
var READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
|
|
153
156
|
function ReaderGreeting({ endpoint = "/api/news/session", className, fallback = "Welcome back", as: Tag = "span" }) {
|
|
154
157
|
const [text, setText] = react.useState(null);
|
|
155
158
|
react.useEffect(() => {
|
|
156
159
|
let alive = true;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
let generation = 0;
|
|
161
|
+
let authenticated = false;
|
|
162
|
+
let controller;
|
|
163
|
+
let deadline;
|
|
164
|
+
const refresh = (clear = false) => {
|
|
165
|
+
if (!alive) return;
|
|
166
|
+
if (clear) {
|
|
167
|
+
authenticated = false;
|
|
168
|
+
setText(null);
|
|
169
|
+
}
|
|
170
|
+
const current = ++generation;
|
|
171
|
+
controller?.abort();
|
|
172
|
+
if (deadline !== void 0) window.clearTimeout(deadline);
|
|
173
|
+
if (document.visibilityState === "hidden") return;
|
|
174
|
+
const request = new AbortController();
|
|
175
|
+
controller = request;
|
|
176
|
+
const timeout = window.setTimeout(() => {
|
|
177
|
+
if (!alive || current !== generation) return;
|
|
178
|
+
generation++;
|
|
179
|
+
authenticated = false;
|
|
180
|
+
setText(null);
|
|
181
|
+
request.abort();
|
|
182
|
+
}, READER_GREETING_TIMEOUT_MS);
|
|
183
|
+
deadline = timeout;
|
|
184
|
+
fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: request.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
|
|
185
|
+
if (!alive || current !== generation) return;
|
|
186
|
+
authenticated = summary?.authenticated === true;
|
|
187
|
+
setText(authenticated && summary ? greetingText(summary, /* @__PURE__ */ new Date(), fallback) : null);
|
|
188
|
+
}).catch(() => {
|
|
189
|
+
if (!alive || current !== generation) return;
|
|
190
|
+
authenticated = false;
|
|
191
|
+
setText(null);
|
|
192
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
193
|
+
};
|
|
194
|
+
const recheck = () => refresh(true);
|
|
195
|
+
const timer = window.setInterval(() => {
|
|
196
|
+
if (authenticated) refresh();
|
|
197
|
+
}, READER_GREETING_POLL_MS);
|
|
198
|
+
window.addEventListener("focus", recheck);
|
|
199
|
+
document.addEventListener("visibilitychange", recheck);
|
|
200
|
+
document.addEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
|
|
201
|
+
refresh(true);
|
|
163
202
|
return () => {
|
|
164
203
|
alive = false;
|
|
165
|
-
|
|
204
|
+
generation++;
|
|
205
|
+
controller?.abort();
|
|
206
|
+
if (deadline !== void 0) window.clearTimeout(deadline);
|
|
207
|
+
window.clearInterval(timer);
|
|
208
|
+
window.removeEventListener("focus", recheck);
|
|
209
|
+
document.removeEventListener("visibilitychange", recheck);
|
|
210
|
+
document.removeEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
|
|
166
211
|
};
|
|
167
212
|
}, [endpoint, fallback]);
|
|
168
213
|
if (text === null) return null;
|
|
@@ -350,6 +395,9 @@ exports.AccountLinkForm = AccountLinkForm;
|
|
|
350
395
|
exports.AccountSwitchPrompt = AccountSwitchPrompt;
|
|
351
396
|
exports.LEDVERY_BUTTON_LABELS = LEDVERY_BUTTON_LABELS;
|
|
352
397
|
exports.LedveryButton = LedveryButton;
|
|
398
|
+
exports.READER_GREETING_POLL_MS = READER_GREETING_POLL_MS;
|
|
399
|
+
exports.READER_GREETING_TIMEOUT_MS = READER_GREETING_TIMEOUT_MS;
|
|
400
|
+
exports.READER_IDENTITY_CHANGED_EVENT = READER_IDENTITY_CHANGED_EVENT;
|
|
353
401
|
exports.READER_SSO_ATTEMPT_WINDOW_MS = READER_SSO_ATTEMPT_WINDOW_MS;
|
|
354
402
|
exports.READER_SSO_BOT_PATTERN = READER_SSO_BOT_PATTERN;
|
|
355
403
|
exports.READER_SSO_PRIVATE_PATHS = READER_SSO_PRIVATE_PATHS;
|
|
@@ -64,6 +64,9 @@ type SessionSummaryResponse = {
|
|
|
64
64
|
locale?: string;
|
|
65
65
|
time_zone?: string;
|
|
66
66
|
};
|
|
67
|
+
declare const READER_GREETING_POLL_MS = 30000;
|
|
68
|
+
declare const READER_GREETING_TIMEOUT_MS = 8000;
|
|
69
|
+
declare const READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
|
|
67
70
|
declare function ReaderGreeting({ endpoint, className, fallback, as: Tag }: {
|
|
68
71
|
endpoint?: string;
|
|
69
72
|
className?: string;
|
|
@@ -128,4 +131,4 @@ declare function AccountSwitchPrompt({ currentName, pendingName, endpoint, class
|
|
|
128
131
|
onDone?: (result: SwitchResult) => void;
|
|
129
132
|
}): React.JSX.Element;
|
|
130
133
|
|
|
131
|
-
export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
|
134
|
+
export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
|
@@ -64,6 +64,9 @@ type SessionSummaryResponse = {
|
|
|
64
64
|
locale?: string;
|
|
65
65
|
time_zone?: string;
|
|
66
66
|
};
|
|
67
|
+
declare const READER_GREETING_POLL_MS = 30000;
|
|
68
|
+
declare const READER_GREETING_TIMEOUT_MS = 8000;
|
|
69
|
+
declare const READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
|
|
67
70
|
declare function ReaderGreeting({ endpoint, className, fallback, as: Tag }: {
|
|
68
71
|
endpoint?: string;
|
|
69
72
|
className?: string;
|
|
@@ -128,4 +131,4 @@ declare function AccountSwitchPrompt({ currentName, pendingName, endpoint, class
|
|
|
128
131
|
onDone?: (result: SwitchResult) => void;
|
|
129
132
|
}): React.JSX.Element;
|
|
130
133
|
|
|
131
|
-
export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
|
134
|
+
export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
|
@@ -148,19 +148,64 @@ function greetingText(profile, now = /* @__PURE__ */ new Date(), fallback = "Wel
|
|
|
148
148
|
if (!name) return fallback;
|
|
149
149
|
return `${greetingWord(localHour(now, profile.time_zone, profile.locale))}, ${name}`;
|
|
150
150
|
}
|
|
151
|
+
var READER_GREETING_POLL_MS = 3e4;
|
|
152
|
+
var READER_GREETING_TIMEOUT_MS = 8e3;
|
|
153
|
+
var READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
|
|
151
154
|
function ReaderGreeting({ endpoint = "/api/news/session", className, fallback = "Welcome back", as: Tag = "span" }) {
|
|
152
155
|
const [text, setText] = useState(null);
|
|
153
156
|
useEffect(() => {
|
|
154
157
|
let alive = true;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
let generation = 0;
|
|
159
|
+
let authenticated = false;
|
|
160
|
+
let controller;
|
|
161
|
+
let deadline;
|
|
162
|
+
const refresh = (clear = false) => {
|
|
163
|
+
if (!alive) return;
|
|
164
|
+
if (clear) {
|
|
165
|
+
authenticated = false;
|
|
166
|
+
setText(null);
|
|
167
|
+
}
|
|
168
|
+
const current = ++generation;
|
|
169
|
+
controller?.abort();
|
|
170
|
+
if (deadline !== void 0) window.clearTimeout(deadline);
|
|
171
|
+
if (document.visibilityState === "hidden") return;
|
|
172
|
+
const request = new AbortController();
|
|
173
|
+
controller = request;
|
|
174
|
+
const timeout = window.setTimeout(() => {
|
|
175
|
+
if (!alive || current !== generation) return;
|
|
176
|
+
generation++;
|
|
177
|
+
authenticated = false;
|
|
178
|
+
setText(null);
|
|
179
|
+
request.abort();
|
|
180
|
+
}, READER_GREETING_TIMEOUT_MS);
|
|
181
|
+
deadline = timeout;
|
|
182
|
+
fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: request.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
|
|
183
|
+
if (!alive || current !== generation) return;
|
|
184
|
+
authenticated = summary?.authenticated === true;
|
|
185
|
+
setText(authenticated && summary ? greetingText(summary, /* @__PURE__ */ new Date(), fallback) : null);
|
|
186
|
+
}).catch(() => {
|
|
187
|
+
if (!alive || current !== generation) return;
|
|
188
|
+
authenticated = false;
|
|
189
|
+
setText(null);
|
|
190
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
191
|
+
};
|
|
192
|
+
const recheck = () => refresh(true);
|
|
193
|
+
const timer = window.setInterval(() => {
|
|
194
|
+
if (authenticated) refresh();
|
|
195
|
+
}, READER_GREETING_POLL_MS);
|
|
196
|
+
window.addEventListener("focus", recheck);
|
|
197
|
+
document.addEventListener("visibilitychange", recheck);
|
|
198
|
+
document.addEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
|
|
199
|
+
refresh(true);
|
|
161
200
|
return () => {
|
|
162
201
|
alive = false;
|
|
163
|
-
|
|
202
|
+
generation++;
|
|
203
|
+
controller?.abort();
|
|
204
|
+
if (deadline !== void 0) window.clearTimeout(deadline);
|
|
205
|
+
window.clearInterval(timer);
|
|
206
|
+
window.removeEventListener("focus", recheck);
|
|
207
|
+
document.removeEventListener("visibilitychange", recheck);
|
|
208
|
+
document.removeEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
|
|
164
209
|
};
|
|
165
210
|
}, [endpoint, fallback]);
|
|
166
211
|
if (text === null) return null;
|
|
@@ -344,4 +389,4 @@ function AccountSwitchPrompt({
|
|
|
344
389
|
] });
|
|
345
390
|
}
|
|
346
391
|
|
|
347
|
-
export { AccountLinkForm, AccountSwitchPrompt, LEDVERY_BUTTON_LABELS, LedveryButton, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
|
392
|
+
export { AccountLinkForm, AccountSwitchPrompt, LEDVERY_BUTTON_LABELS, LedveryButton, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
|
package/dist/reader-sso.cjs
CHANGED
|
@@ -195,7 +195,7 @@ async function startSignIn(config, input) {
|
|
|
195
195
|
}
|
|
196
196
|
const staleSession = [];
|
|
197
197
|
if (input.cookies(c.names.session)) {
|
|
198
|
-
const current = await currentUser(c, input.cookies(c.names.session));
|
|
198
|
+
const current = await currentUser(c, input.cookies(c.names.session), mode === "silent" ? "passive" : "active");
|
|
199
199
|
if (current && current.email_verified) {
|
|
200
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: [] };
|
|
201
201
|
} else {
|
|
@@ -237,9 +237,9 @@ async function startSignIn(config, input) {
|
|
|
237
237
|
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: [] };
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
async function currentUser(c, token) {
|
|
240
|
+
async function currentUser(c, token, activity = "active") {
|
|
241
241
|
if (!token || !SESSION_TOKEN.test(token)) return null;
|
|
242
|
-
const response = await c.request("/v1/auth/me", { method: "GET", token });
|
|
242
|
+
const response = await c.request(activity === "passive" ? "/v1/auth/me?passive=true" : "/v1/auth/me", { method: "GET", token });
|
|
243
243
|
if (failed(response)) return null;
|
|
244
244
|
const me = payload(response);
|
|
245
245
|
if (typeof me.id !== "string" || !me.id) return null;
|
|
@@ -468,7 +468,7 @@ async function signOut(config, input) {
|
|
|
468
468
|
}
|
|
469
469
|
async function sessionSummary(config, cookies) {
|
|
470
470
|
const c = resolve(config);
|
|
471
|
-
const user = await currentUser(c, cookies(c.names.session));
|
|
471
|
+
const user = await currentUser(c, cookies(c.names.session), "passive");
|
|
472
472
|
if (!user || !user.email_verified) return { authenticated: false };
|
|
473
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 } : {} };
|
|
474
474
|
}
|
package/dist/reader-sso.js
CHANGED
|
@@ -180,7 +180,7 @@ async function startSignIn(config, input) {
|
|
|
180
180
|
}
|
|
181
181
|
const staleSession = [];
|
|
182
182
|
if (input.cookies(c.names.session)) {
|
|
183
|
-
const current = await currentUser(c, input.cookies(c.names.session));
|
|
183
|
+
const current = await currentUser(c, input.cookies(c.names.session), mode === "silent" ? "passive" : "active");
|
|
184
184
|
if (current && current.email_verified) {
|
|
185
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: [] };
|
|
186
186
|
} else {
|
|
@@ -222,9 +222,9 @@ async function startSignIn(config, input) {
|
|
|
222
222
|
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: [] };
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
-
async function currentUser(c, token) {
|
|
225
|
+
async function currentUser(c, token, activity = "active") {
|
|
226
226
|
if (!token || !SESSION_TOKEN.test(token)) return null;
|
|
227
|
-
const response = await c.request("/v1/auth/me", { method: "GET", token });
|
|
227
|
+
const response = await c.request(activity === "passive" ? "/v1/auth/me?passive=true" : "/v1/auth/me", { method: "GET", token });
|
|
228
228
|
if (failed(response)) return null;
|
|
229
229
|
const me = payload(response);
|
|
230
230
|
if (typeof me.id !== "string" || !me.id) return null;
|
|
@@ -453,7 +453,7 @@ async function signOut(config, input) {
|
|
|
453
453
|
}
|
|
454
454
|
async function sessionSummary(config, cookies) {
|
|
455
455
|
const c = resolve(config);
|
|
456
|
-
const user = await currentUser(c, cookies(c.names.session));
|
|
456
|
+
const user = await currentUser(c, cookies(c.names.session), "passive");
|
|
457
457
|
if (!user || !user.email_verified) return { authenticated: false };
|
|
458
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 } : {} };
|
|
459
459
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.77",
|
|
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
|
+
"bin": {
|
|
7
|
+
"scalemule-article": "./bin/scalemule-article.mjs"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
7
10
|
"module": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -197,6 +200,7 @@
|
|
|
197
200
|
}
|
|
198
201
|
},
|
|
199
202
|
"files": [
|
|
203
|
+
"bin",
|
|
200
204
|
"dist",
|
|
201
205
|
"tokens",
|
|
202
206
|
"marketplace.css",
|