@scalemule/news-publication 0.0.67 → 0.0.68

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 CHANGED
@@ -305,3 +305,15 @@ The publisher-supplied `PawMark` preserves the approved SVG paths. Hosts may set
305
305
  ### Homepage image identity
306
306
 
307
307
  `NewsItem.visual_identity` is a versioned public identity from the Blog feed. `composeEdition` allocates covers across the entire bounded inventory, gives the selected lead priority, and holds repeated covers as `null` without dropping stories or mutating inputs. Never re-add duplicate images as fallback. Legacy Photo rendition URLs are normalized by tenant/asset ID; server-side Photo identity detects copies under different IDs and ordinary resize/re-encoding/crop variants. Do not fetch or decode images in page components. Preserve existing caption/credit policy. See platform `docs/HOMEPAGE-PHOTO-IDENTITY.md` for storage, rights, concurrency and rollout tests.
308
+
309
+ ### Standard article submission
310
+
311
+ Server tools and authorized agents use `PublicationArticleClient` from
312
+ `@scalemule/news-publication/article-workflow`. Discover `options()` first;
313
+ require `workflow_version: 1`. Use `submitArticle()` for a private draft plus
314
+ managed photo staging, or `get()` and `save()` for a generation-fenced edit.
315
+ Keep the operation key when resuming. Editorial approval and release remain
316
+ explicit commands; acceptance does not certify scan, image or audio completion.
317
+ The platform contract and a complete example live in
318
+ `docs/news-platform/ARTICLE-WORKFLOW-V1.md` in the ScaleMule meta-repo.
319
+ Do not use direct SQL, S3 paths, base64 bodies or invented media readiness.
@@ -0,0 +1,21 @@
1
+ /** Declarative presentation resolved by Blog from the article's pinned profile version. */
2
+ type ArticleFormat = "news" | "brief" | "explainer" | "interview" | "opinion" | "photo_essay" | "video_story";
3
+ type ArticleLayout = {
4
+ id: string;
5
+ version: number;
6
+ name: string;
7
+ renderer: "standard" | "feature" | "portrait";
8
+ hero_aspect: "landscape" | "portrait" | "square";
9
+ headline_position: "before_hero" | "after_hero";
10
+ wide_body: boolean;
11
+ };
12
+ type ArticlePresentation = {
13
+ format?: ArticleFormat | null;
14
+ layout: ArticleLayout;
15
+ };
16
+ /** Never turn an arbitrary API field into CSS, HTML, a class name or a URL. */
17
+ declare function articlePresentation(value?: ArticlePresentation | null): ArticlePresentation;
18
+ declare function articleHeroRatio(layout: ArticleLayout): number;
19
+ declare function articleFormatLabel(format?: ArticleFormat | null): string | undefined;
20
+
21
+ export { type ArticleFormat as A, type ArticlePresentation as a, type ArticleLayout as b, articleFormatLabel as c, articleHeroRatio as d, articlePresentation as e };
@@ -0,0 +1,21 @@
1
+ /** Declarative presentation resolved by Blog from the article's pinned profile version. */
2
+ type ArticleFormat = "news" | "brief" | "explainer" | "interview" | "opinion" | "photo_essay" | "video_story";
3
+ type ArticleLayout = {
4
+ id: string;
5
+ version: number;
6
+ name: string;
7
+ renderer: "standard" | "feature" | "portrait";
8
+ hero_aspect: "landscape" | "portrait" | "square";
9
+ headline_position: "before_hero" | "after_hero";
10
+ wide_body: boolean;
11
+ };
12
+ type ArticlePresentation = {
13
+ format?: ArticleFormat | null;
14
+ layout: ArticleLayout;
15
+ };
16
+ /** Never turn an arbitrary API field into CSS, HTML, a class name or a URL. */
17
+ declare function articlePresentation(value?: ArticlePresentation | null): ArticlePresentation;
18
+ declare function articleHeroRatio(layout: ArticleLayout): number;
19
+ declare function articleFormatLabel(format?: ArticleFormat | null): string | undefined;
20
+
21
+ export { type ArticleFormat as A, type ArticlePresentation as a, type ArticleLayout as b, articleFormatLabel as c, articleHeroRatio as d, articlePresentation as e };
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ // src/article-workflow-client.ts
4
+ var ArticleWorkflowError = class extends Error {
5
+ constructor(message, status, articleId) {
6
+ super(message);
7
+ this.status = status;
8
+ this.articleId = articleId;
9
+ this.name = "ArticleWorkflowError";
10
+ }
11
+ status;
12
+ articleId;
13
+ };
14
+ var PublicationArticleClient = class {
15
+ constructor(config) {
16
+ this.config = config;
17
+ const url = new URL(config.apiUrl);
18
+ if (url.protocol !== "https:" && !["localhost", "127.0.0.1"].includes(url.hostname))
19
+ throw new Error("Use an HTTPS API endpoint");
20
+ this.base = config.apiUrl.replace(/\/$/, "") + `/v1/blog/publications/${encodeURIComponent(config.publicationId)}`;
21
+ }
22
+ config;
23
+ base;
24
+ async request(path, method = "GET", body, extra) {
25
+ const headers = new Headers(extra);
26
+ headers.set("accept", "application/json");
27
+ if (this.config.apiKey) headers.set("x-api-key", this.config.apiKey);
28
+ if (this.config.accessToken)
29
+ headers.set("authorization", `Bearer ${this.config.accessToken}`);
30
+ const form = body instanceof FormData;
31
+ if (body !== void 0 && !form)
32
+ headers.set("content-type", "application/json");
33
+ const url = new URL(
34
+ path.startsWith("/v1/") ? this.config.apiUrl.replace(/\/$/, "") + path : this.base + path
35
+ );
36
+ if (this.config.applicationId)
37
+ url.searchParams.set("application_id", this.config.applicationId);
38
+ const response = await (this.config.fetch ?? fetch)(url, {
39
+ method,
40
+ headers,
41
+ body: body === void 0 ? void 0 : form ? body : JSON.stringify(body),
42
+ cache: "no-store",
43
+ redirect: "error"
44
+ });
45
+ const value = await response.json();
46
+ if (!response.ok || value.success !== true || value.data == null)
47
+ throw new ArticleWorkflowError(
48
+ value.error?.message || `Article API returned ${response.status}`,
49
+ response.status
50
+ );
51
+ return value.data;
52
+ }
53
+ options(articleId) {
54
+ return this.request(
55
+ `/article-workflow/options${articleId ? `?article_id=${encodeURIComponent(articleId)}` : ""}`
56
+ );
57
+ }
58
+ get(articleId) {
59
+ return this.request(
60
+ `/articles/${encodeURIComponent(articleId)}/collaboration`
61
+ );
62
+ }
63
+ processing(articleId) {
64
+ return this.request(`/articles/${encodeURIComponent(articleId)}/processing`);
65
+ }
66
+ retryProcessing(articleId, expectedVersion, assetSourceFileId) {
67
+ return this.request(
68
+ `/articles/${encodeURIComponent(articleId)}/processing`,
69
+ "POST",
70
+ {
71
+ expected_version: expectedVersion,
72
+ asset_source_file_id: assetSourceFileId
73
+ }
74
+ );
75
+ }
76
+ create(key, authorProfileId, document, reviewPolicy = "SUBJECT_APPROVAL_PLUS_EDITOR") {
77
+ return this.request(
78
+ "/article-workflow",
79
+ "POST",
80
+ {
81
+ operation: "create",
82
+ author_profile_id: authorProfileId,
83
+ document,
84
+ review_policy: reviewPolicy
85
+ },
86
+ { "Idempotency-Key": key }
87
+ );
88
+ }
89
+ save(key, articleId, generation, document, reason = "EDITOR_EDIT") {
90
+ return this.request(
91
+ "/article-workflow",
92
+ "POST",
93
+ {
94
+ operation: "save",
95
+ article_id: articleId,
96
+ expected_generation: generation,
97
+ document,
98
+ reason
99
+ },
100
+ { "Idempotency-Key": key }
101
+ );
102
+ }
103
+ command(articleId, generation, action) {
104
+ return this.request(
105
+ `/articles/${encodeURIComponent(articleId)}/collaboration`,
106
+ "POST",
107
+ { ...action, expected_generation: generation }
108
+ );
109
+ }
110
+ registerPhoto(fileId) {
111
+ return this.request(
112
+ `/v1/photos/${this.config.applicationId ? "admin/register" : "register"}`,
113
+ "POST",
114
+ {
115
+ file_id: fileId,
116
+ content_kind: "article",
117
+ publication_id: this.config.publicationId,
118
+ ...this.config.applicationId ? { application_id: this.config.applicationId } : {}
119
+ }
120
+ );
121
+ }
122
+ upload(articleId, generation, file, filename, metadata) {
123
+ const body = new FormData();
124
+ body.set("metadata", JSON.stringify(metadata));
125
+ body.set("file", file, filename);
126
+ return this.request(`/articles/${encodeURIComponent(articleId)}/assets`, "POST", body, {
127
+ "If-Match": String(generation)
128
+ });
129
+ }
130
+ /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
131
+ * replay the same operation key to inspect/reuse staged contributions before another transfer.
132
+ * Editorial approval and release are always separate explicit operations. */
133
+ async submitArticle(input) {
134
+ if (input.document.media?.length)
135
+ throw new Error(
136
+ "Pass file assets separately when creating an article package"
137
+ );
138
+ const capabilities = await this.options();
139
+ if (capabilities.workflow_version !== 1)
140
+ throw new Error("Unsupported article workflow version");
141
+ if (input.assets?.length && !capabilities.capabilities.managed_private_asset_upload)
142
+ throw new Error("Managed article upload is not deployed");
143
+ const created = await this.create(
144
+ input.operationKey,
145
+ input.authorProfileId,
146
+ input.document,
147
+ input.reviewPolicy
148
+ );
149
+ try {
150
+ const selected = [];
151
+ for (const asset of input.assets ?? []) {
152
+ let current = await this.get(created.article_id);
153
+ const digest = Array.from(
154
+ new Uint8Array(
155
+ await crypto.subtle.digest(
156
+ "SHA-256",
157
+ await asset.file.arrayBuffer()
158
+ )
159
+ )
160
+ ).map((v) => v.toString(16).padStart(2, "0")).join("");
161
+ const existing = current.contributions.find(
162
+ (c) => c.kind === "MEDIA" && c.state !== "REJECTED" && c.state !== "WITHDRAWN" && c.payload.sha256 === digest && Object.entries(asset.metadata).every(
163
+ ([k, v]) => c.payload[k] === v
164
+ )
165
+ );
166
+ const staged = existing?.payload.file_id ? { contribution_id: existing.id, file_id: existing.payload.file_id } : await this.upload(
167
+ created.article_id,
168
+ current.generation,
169
+ asset.file,
170
+ asset.filename,
171
+ asset.metadata
172
+ );
173
+ await this.registerPhoto(staged.file_id);
174
+ if (input.approveMedia) {
175
+ if (!asset.metadata.rights_confirmed || asset.metadata.private_preview_only)
176
+ throw new Error("Publication requires confirmed image rights");
177
+ if (existing?.state !== "APPROVED") {
178
+ current = await this.get(created.article_id);
179
+ await this.command(created.article_id, current.generation, {
180
+ action: "RESOLVE",
181
+ contribution_id: staged.contribution_id,
182
+ state: "APPROVED"
183
+ });
184
+ }
185
+ selected.push({
186
+ kind: "image",
187
+ contribution_id: staged.contribution_id,
188
+ file_id: staged.file_id,
189
+ caption: asset.metadata.caption,
190
+ credit: asset.metadata.credit,
191
+ alt: asset.metadata.alt,
192
+ position: asset.position
193
+ });
194
+ }
195
+ }
196
+ if (selected.length) {
197
+ const current = await this.get(created.article_id);
198
+ const document = { ...input.document, media: selected };
199
+ const saved = current.revisions.find(
200
+ (r) => r.id === current.current_revision_id
201
+ )?.document;
202
+ if (!saved) throw new Error("Current immutable revision is missing");
203
+ if (sameDocument(saved, document))
204
+ return {
205
+ ...created,
206
+ revision_id: current.current_revision_id,
207
+ generation: current.generation,
208
+ editorial_status: current.status,
209
+ replayed: true
210
+ };
211
+ if (!sameDocument(
212
+ { ...saved, media: [] },
213
+ { ...input.document, media: [] }
214
+ ) || saved.media?.length)
215
+ throw new ArticleWorkflowError(
216
+ "The article was edited after this package began; load its current revision before changing it",
217
+ 409,
218
+ created.article_id
219
+ );
220
+ return await this.save(
221
+ `${input.operationKey}:assets`,
222
+ created.article_id,
223
+ current.generation,
224
+ document,
225
+ "MEDIA_UPDATE"
226
+ );
227
+ }
228
+ return created;
229
+ } catch (error) {
230
+ if (error instanceof ArticleWorkflowError) {
231
+ error.articleId = created.article_id;
232
+ throw error;
233
+ }
234
+ throw new ArticleWorkflowError(
235
+ error instanceof Error ? error.message : "Article package interrupted",
236
+ 0,
237
+ created.article_id
238
+ );
239
+ }
240
+ }
241
+ };
242
+ function sameDocument(a, b) {
243
+ const canonical = (d) => [
244
+ d.title,
245
+ d.subtitle || "",
246
+ d.content_markdown,
247
+ d.section,
248
+ d.community || "",
249
+ d.format || null,
250
+ d.layout ? [d.layout.id, d.layout.version] : null,
251
+ (d.media || []).map((m) => [
252
+ m.kind || "image",
253
+ m.contribution_id,
254
+ m.file_id,
255
+ m.caption,
256
+ m.credit,
257
+ m.alt,
258
+ m.position
259
+ ])
260
+ ];
261
+ return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
262
+ }
263
+
264
+ exports.ArticleWorkflowError = ArticleWorkflowError;
265
+ exports.PublicationArticleClient = PublicationArticleClient;
@@ -0,0 +1,131 @@
1
+ import { A as ArticleFormat } from './article-presentation-0d1FiDgA.cjs';
2
+
3
+ type ArticleDocument = {
4
+ title: string;
5
+ subtitle?: string;
6
+ content_markdown: string;
7
+ section: string;
8
+ community?: string;
9
+ format?: ArticleFormat;
10
+ layout?: {
11
+ id: string;
12
+ version: number;
13
+ };
14
+ media?: ArticleMedia[];
15
+ };
16
+ type ArticleMedia = {
17
+ kind?: "image" | "video";
18
+ contribution_id: string;
19
+ file_id: string;
20
+ caption: string;
21
+ credit: string;
22
+ alt: string;
23
+ position: "hero" | "inline" | "gallery" | "attachment";
24
+ };
25
+ type AssetMetadata = {
26
+ caption: string;
27
+ credit: string;
28
+ alt: string;
29
+ creator: string;
30
+ who_is_pictured: string;
31
+ when_taken: string;
32
+ where_taken: string;
33
+ rights_confirmed: boolean;
34
+ private_preview_only?: boolean;
35
+ };
36
+ type ArticleOperation = {
37
+ article_id: string;
38
+ revision_id: string;
39
+ generation: number;
40
+ editorial_status: string;
41
+ permalink_path: string;
42
+ replayed: boolean;
43
+ };
44
+ type Record = {
45
+ post_id: string;
46
+ current_revision_id: string;
47
+ generation: number;
48
+ status: string;
49
+ revisions: {
50
+ id: string;
51
+ document: ArticleDocument;
52
+ }[];
53
+ contributions: {
54
+ id: string;
55
+ kind: string;
56
+ state: string;
57
+ payload: Partial<AssetMetadata> & {
58
+ file_id?: string;
59
+ sha256?: string;
60
+ };
61
+ }[];
62
+ };
63
+ declare class ArticleWorkflowError extends Error {
64
+ status: number;
65
+ articleId?: string | undefined;
66
+ constructor(message: string, status: number, articleId?: string | undefined);
67
+ }
68
+ /** Server/CLI client. Credentials belong to the caller and are never embedded in a publication bundle. */
69
+ declare class PublicationArticleClient {
70
+ private config;
71
+ private base;
72
+ constructor(config: {
73
+ apiUrl: string;
74
+ publicationId: string;
75
+ apiKey?: string;
76
+ accessToken?: string;
77
+ applicationId?: string;
78
+ fetch?: typeof fetch;
79
+ });
80
+ private request;
81
+ options(articleId?: string): Promise<{
82
+ workflow_version: number;
83
+ formats: string[];
84
+ layouts: unknown[];
85
+ capabilities: {
86
+ managed_private_asset_upload: boolean;
87
+ };
88
+ }>;
89
+ get(articleId: string): Promise<Record>;
90
+ processing(articleId: string): Promise<{
91
+ narration: {
92
+ status: string;
93
+ error_code?: string;
94
+ };
95
+ visual: {
96
+ status: string;
97
+ };
98
+ }>;
99
+ retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string): Promise<unknown>;
100
+ create(key: string, authorProfileId: string, document: ArticleDocument, reviewPolicy?: string): Promise<ArticleOperation>;
101
+ save(key: string, articleId: string, generation: number, document: ArticleDocument, reason?: string): Promise<ArticleOperation>;
102
+ command(articleId: string, generation: number, action: object): Promise<unknown>;
103
+ registerPhoto(fileId: string): Promise<{
104
+ id?: string;
105
+ status?: string;
106
+ }>;
107
+ upload(articleId: string, generation: number, file: Blob, filename: string, metadata: AssetMetadata): Promise<{
108
+ contribution_id: string;
109
+ file_id: string;
110
+ kind: "image";
111
+ state: "OPEN";
112
+ }>;
113
+ /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
114
+ * replay the same operation key to inspect/reuse staged contributions before another transfer.
115
+ * Editorial approval and release are always separate explicit operations. */
116
+ submitArticle(input: {
117
+ operationKey: string;
118
+ authorProfileId: string;
119
+ document: ArticleDocument;
120
+ reviewPolicy?: string;
121
+ assets?: {
122
+ file: Blob;
123
+ filename: string;
124
+ metadata: AssetMetadata;
125
+ position: ArticleMedia["position"];
126
+ }[];
127
+ approveMedia?: boolean;
128
+ }): Promise<ArticleOperation>;
129
+ }
130
+
131
+ export { type ArticleDocument, type ArticleMedia, type ArticleOperation, ArticleWorkflowError, type AssetMetadata, PublicationArticleClient };
@@ -0,0 +1,131 @@
1
+ import { A as ArticleFormat } from './article-presentation-0d1FiDgA.js';
2
+
3
+ type ArticleDocument = {
4
+ title: string;
5
+ subtitle?: string;
6
+ content_markdown: string;
7
+ section: string;
8
+ community?: string;
9
+ format?: ArticleFormat;
10
+ layout?: {
11
+ id: string;
12
+ version: number;
13
+ };
14
+ media?: ArticleMedia[];
15
+ };
16
+ type ArticleMedia = {
17
+ kind?: "image" | "video";
18
+ contribution_id: string;
19
+ file_id: string;
20
+ caption: string;
21
+ credit: string;
22
+ alt: string;
23
+ position: "hero" | "inline" | "gallery" | "attachment";
24
+ };
25
+ type AssetMetadata = {
26
+ caption: string;
27
+ credit: string;
28
+ alt: string;
29
+ creator: string;
30
+ who_is_pictured: string;
31
+ when_taken: string;
32
+ where_taken: string;
33
+ rights_confirmed: boolean;
34
+ private_preview_only?: boolean;
35
+ };
36
+ type ArticleOperation = {
37
+ article_id: string;
38
+ revision_id: string;
39
+ generation: number;
40
+ editorial_status: string;
41
+ permalink_path: string;
42
+ replayed: boolean;
43
+ };
44
+ type Record = {
45
+ post_id: string;
46
+ current_revision_id: string;
47
+ generation: number;
48
+ status: string;
49
+ revisions: {
50
+ id: string;
51
+ document: ArticleDocument;
52
+ }[];
53
+ contributions: {
54
+ id: string;
55
+ kind: string;
56
+ state: string;
57
+ payload: Partial<AssetMetadata> & {
58
+ file_id?: string;
59
+ sha256?: string;
60
+ };
61
+ }[];
62
+ };
63
+ declare class ArticleWorkflowError extends Error {
64
+ status: number;
65
+ articleId?: string | undefined;
66
+ constructor(message: string, status: number, articleId?: string | undefined);
67
+ }
68
+ /** Server/CLI client. Credentials belong to the caller and are never embedded in a publication bundle. */
69
+ declare class PublicationArticleClient {
70
+ private config;
71
+ private base;
72
+ constructor(config: {
73
+ apiUrl: string;
74
+ publicationId: string;
75
+ apiKey?: string;
76
+ accessToken?: string;
77
+ applicationId?: string;
78
+ fetch?: typeof fetch;
79
+ });
80
+ private request;
81
+ options(articleId?: string): Promise<{
82
+ workflow_version: number;
83
+ formats: string[];
84
+ layouts: unknown[];
85
+ capabilities: {
86
+ managed_private_asset_upload: boolean;
87
+ };
88
+ }>;
89
+ get(articleId: string): Promise<Record>;
90
+ processing(articleId: string): Promise<{
91
+ narration: {
92
+ status: string;
93
+ error_code?: string;
94
+ };
95
+ visual: {
96
+ status: string;
97
+ };
98
+ }>;
99
+ retryProcessing(articleId: string, expectedVersion: number, assetSourceFileId?: string): Promise<unknown>;
100
+ create(key: string, authorProfileId: string, document: ArticleDocument, reviewPolicy?: string): Promise<ArticleOperation>;
101
+ save(key: string, articleId: string, generation: number, document: ArticleDocument, reason?: string): Promise<ArticleOperation>;
102
+ command(articleId: string, generation: number, action: object): Promise<unknown>;
103
+ registerPhoto(fileId: string): Promise<{
104
+ id?: string;
105
+ status?: string;
106
+ }>;
107
+ upload(articleId: string, generation: number, file: Blob, filename: string, metadata: AssetMetadata): Promise<{
108
+ contribution_id: string;
109
+ file_id: string;
110
+ kind: "image";
111
+ state: "OPEN";
112
+ }>;
113
+ /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
114
+ * replay the same operation key to inspect/reuse staged contributions before another transfer.
115
+ * Editorial approval and release are always separate explicit operations. */
116
+ submitArticle(input: {
117
+ operationKey: string;
118
+ authorProfileId: string;
119
+ document: ArticleDocument;
120
+ reviewPolicy?: string;
121
+ assets?: {
122
+ file: Blob;
123
+ filename: string;
124
+ metadata: AssetMetadata;
125
+ position: ArticleMedia["position"];
126
+ }[];
127
+ approveMedia?: boolean;
128
+ }): Promise<ArticleOperation>;
129
+ }
130
+
131
+ export { type ArticleDocument, type ArticleMedia, type ArticleOperation, ArticleWorkflowError, type AssetMetadata, PublicationArticleClient };