@scalemule/news-publication 0.0.81 → 0.0.82

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
@@ -328,15 +328,54 @@ The publisher-supplied `PawMark` preserves the approved SVG paths. Hosts may set
328
328
 
329
329
  ### Standard article submission
330
330
 
331
- Server tools and authorized agents use `PublicationArticleClient` from
332
- `@scalemule/news-publication/article-workflow`. Discover `options()` first;
333
- require `workflow_version: 1`. Use `submitArticle()` for a private draft plus
334
- managed photo staging, or `get()` and `save()` for a generation-fenced edit.
335
- Keep the operation key when resuming. Editorial approval and release remain
336
- explicit commands; acceptance does not certify scan, image or audio completion.
337
- The platform contract and a complete example live in
338
- `docs/news-platform/ARTICLE-WORKFLOW-V1.md` in the ScaleMule meta-repo.
339
- Do not use direct SQL, S3 paths, base64 bodies or invented media readiness.
331
+ Submit through the server-owned API using `PublicationArticleClient` from
332
+ `@scalemule/news-publication/article-workflow`:
333
+
334
+ ```ts
335
+ const receipt = await client.submitArticle({
336
+ operationKey: "story-intake-v1", authorProfileId: authorId,
337
+ document: { title, content_markdown: body, section: "Local Business" },
338
+ intent: "publish",
339
+ assets: [{ file: photo, filename: "venue.jpg", position: "hero",
340
+ metadata: { alt: "Exterior of the actual venue", credit: "Photographer",
341
+ rights_confirmed: true } }],
342
+ });
343
+ ```
344
+
345
+ This sends **one POST** to `/v1/blog/publications/{id}/submissions`, with a
346
+ multipart manifest and its files. JSON without files is supported by
347
+ `client.submit(manifest, key)`. The same API accepts `article.operation: "save"`
348
+ with the existing article ID, expected generation, complete document and revision
349
+ reason. The server owns private Photo/Storage intake, scan checks, immutable
350
+ media attachment, required private narration and release. Optional narration
351
+ and missing-visual commissioning use the existing post-publication workers.
352
+ Existing subject/editorial/sensitivity approvals still apply. `draft` is the
353
+ safe default; `publish` requests release after the configured gates.
354
+
355
+ An accepted receipt is **not** a claim that media is ready or a page has been
356
+ verified. Read `client.submission(receipt.submission_id)` for holds, processing
357
+ and public readiness. After a timeout, replay the identical manifest, files and
358
+ operation key. Do not re-upload through other endpoints or create a new article.
359
+ A foreign revision, rejected media, expired replay or unconfirmed asset intake
360
+ stops with a retained receipt rather than silently overwriting or duplicating.
361
+ No client-side fallback performs a separate multi-service publishing sequence.
362
+ Discover `managed_submission_version: 1` before adopting this endpoint on an
363
+ older server. Supported files are still JPEG/PNG/WebP photographs, not video or
364
+ animation. Never use SQL, S3 paths, embedded binary or invented ready flags.
365
+
366
+ CLI equivalents:
367
+
368
+ ```sh
369
+ scalemule-article submit --publication "$PUBLICATION_ID" \
370
+ --manifest article.json --asset hero=./venue.jpg --operation-key story-intake-v1
371
+ scalemule-article status --publication "$PUBLICATION_ID" --submission "$SUBMISSION_ID"
372
+ ```
373
+
374
+ The manifest uses the API's snake_case fields: `article`, `intent`, and optional
375
+ `assets: [{slot: "hero", position: "hero", metadata: {...}}]`. Each `--asset`
376
+ matches one declared slot. Credentials are normal project/member credentials.
377
+ The operation key is retained for 30 days. A legacy article must first have an
378
+ explicitly selected collaboration policy; this endpoint never downgrades review.
340
379
 
341
380
  ### Edit article prose, cards and citations
342
381
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, mkdir, writeFile, rename, open, unlink } from 'node:fs/promises';
2
+ import { readFile, stat, mkdir, writeFile, rename, open, unlink } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join, basename } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
@@ -7,6 +7,8 @@ import { parseArgs } from 'node:util';
7
7
  import { PublicationArticleClient, articlePhotoHtmlMatches,articleEditHtmlMatches } from '../dist/article-workflow-client.js';
8
8
 
9
9
  const help = `Usage:
10
+ scalemule-article submit --publication ID --manifest article.json --operation-key KEY [--asset hero=/path/photo.jpg]
11
+ scalemule-article status --publication ID --submission ID
10
12
  scalemule-article inspect --publication ID --article ID
11
13
  scalemule-article replace-cover --publication ID --article ID --file-id ID --expected-version N --metadata photo.json
12
14
  scalemule-article replace-photo --url ARTICLE_URL --file PHOTO --metadata photo.json --operation-key KEY
@@ -54,7 +56,7 @@ that every website/CDN cache already displays the new image.
54
56
  try {
55
57
  const { values, positionals } = parseArgs({
56
58
  allowPositionals: true, strict: true,
57
- options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url', 'url', 'file', 'operation-key', 'state', 'timeout-ms','patch','expected-generation','enable-review-policy'].map(k => [k, { type: 'string' }]).concat([['help', { type: 'boolean' }],['release',{type:'boolean'}]])),
59
+ options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url', 'url', 'file', 'operation-key', 'state', 'timeout-ms','patch','expected-generation','enable-review-policy','manifest','submission'].map(k => [k, { type: 'string' }]).concat([['asset',{type:'string',multiple:true}],['help', { type: 'boolean' }],['release',{type:'boolean'}]])),
58
60
  });
59
61
  if (values.help || !positionals.length) {
60
62
  console.log(help);
@@ -64,7 +66,7 @@ try {
64
66
  const budget = Number(values['timeout-ms'] ?? (command==='edit'?10000:30000));
65
67
  if (!Number.isFinite(budget) || budget < 100 || budget > 120000) throw Error('--timeout-ms must be between 100 and 120000');
66
68
  const remaining = () => { const ms = Math.floor(budget - (performance.now()-commandStarted)); if (ms < 1) throw Error('Operation deadline reached; resume the same operation'); return ms; };
67
- if (extra.length || !['inspect', 'replace-cover', 'replace-photo','edit'].includes(command)) throw Error('Use inspect, edit, replace-cover or replace-photo; see --help');
69
+ if (extra.length || !['submit','status','inspect', 'replace-cover', 'replace-photo','edit'].includes(command)) throw Error('Use submit, status, inspect, edit, replace-cover or replace-photo; see --help');
68
70
  if (values.url) {
69
71
  if (!process.env.SCALEMULE_API_KEY || !process.env.SCALEMULE_ACCESS_TOKEN || values['application-id']) throw Error('URL resolution requires normal publication member/API-key credentials');
70
72
  const url = new URL(values.url);
@@ -83,7 +85,7 @@ try {
83
85
  if ((values.publication && values.publication !== pub.id) || (values.article && values.article !== resolved.article.id)) throw Error('Article URL and supplied IDs disagree');
84
86
  values.publication = pub.id; values.article = resolved.article.id;
85
87
  }
86
- for (const key of ['publication', 'article']) if (!values[key]) throw Error(`Missing --${key}`);
88
+ for (const key of (['submit','status'].includes(command)?['publication']:['publication','article'])) if (!values[key]) throw Error(`Missing --${key}`);
87
89
  if (!process.env.SCALEMULE_ACCESS_TOKEN) throw Error('SCALEMULE_ACCESS_TOKEN is required; use normal member or platform admin authorization');
88
90
  if (!values['application-id'] && !process.env.SCALEMULE_API_KEY) throw Error('Member calls also require SCALEMULE_API_KEY');
89
91
  const client = new PublicationArticleClient({
@@ -94,7 +96,22 @@ try {
94
96
  accessToken: process.env.SCALEMULE_ACCESS_TOKEN,
95
97
  });
96
98
  const started = performance.now();
97
- if (command === 'inspect') {
99
+ if(command==='submit'){
100
+ for(const key of ['manifest','operation-key'])if(!values[key])throw Error('Missing --'+key);
101
+ const manifest=JSON.parse(await readFile(values.manifest,'utf8'));
102
+ const assets=[];
103
+ for(const entry of values.asset??[]){
104
+ const split=entry.indexOf('=');if(split<1)throw Error('--asset must be slot=/path/to/photo');
105
+ const slot=entry.slice(0,split),path=entry.slice(split+1);
106
+ const info=await stat(path);if(!info.isFile()||info.size<1||info.size>25*1024*1024)throw Error('Photo must be a regular file between 1 byte and 25 MiB');
107
+ assets.push({slot,filename:basename(path),file:new Blob([await readFile(path)])});
108
+ }
109
+ const receipt=await client.submit(manifest,values['operation-key'],assets,AbortSignal.timeout(remaining()));
110
+ console.log(JSON.stringify({...receipt,elapsed_ms:Math.round(performance.now()-started)},null,2));
111
+ }else if(command==='status'){
112
+ if(!values.submission)throw Error('Missing --submission');
113
+ console.log(JSON.stringify(await client.submission(values.submission,AbortSignal.timeout(remaining())),null,2));
114
+ }else if (command === 'inspect') {
98
115
  const [options, processing, collaboration] = await Promise.all([
99
116
  client.options(values.article), client.processing(values.article),
100
117
  client.get(values.article).then(current => ({ enabled: true, generation: current.generation, status: current.status })).catch(error => {
@@ -900,143 +900,35 @@ var PublicationArticleClient = class {
900
900
  "If-Match": String(generation)
901
901
  });
902
902
  }
903
- /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
904
- * replay the same operation key to inspect/reuse staged contributions before another transfer.
905
- * Editorial approval and release are always separate explicit operations. */
906
- async submitArticle(input) {
907
- if (input.document.media?.length)
908
- throw new Error(
909
- "Pass file assets separately when creating an article package"
910
- );
911
- const capabilities = await this.options();
912
- if (capabilities.workflow_version !== 1)
913
- throw new Error("Unsupported article workflow version");
914
- if (input.assets?.length && !capabilities.capabilities.managed_private_asset_upload)
915
- throw new Error("Managed article upload is not deployed");
916
- const created = await this.create(
917
- input.operationKey,
918
- input.authorProfileId,
919
- input.document,
920
- input.reviewPolicy
921
- );
922
- try {
923
- const selected = [];
924
- for (const asset of input.assets ?? []) {
925
- let current = await this.get(created.article_id);
926
- const digest3 = Array.from(
927
- new Uint8Array(
928
- await crypto.subtle.digest(
929
- "SHA-256",
930
- await asset.file.arrayBuffer()
931
- )
932
- )
933
- ).map((v) => v.toString(16).padStart(2, "0")).join("");
934
- const existing = current.contributions.find(
935
- (c) => c.kind === "MEDIA" && c.state !== "REJECTED" && c.state !== "WITHDRAWN" && c.payload.sha256 === digest3 && Object.entries(asset.metadata).every(
936
- ([k, v]) => c.payload[k] === v
937
- )
938
- );
939
- const staged = existing?.payload.file_id ? { contribution_id: existing.id, file_id: existing.payload.file_id } : await this.upload(
940
- created.article_id,
941
- current.generation,
942
- asset.file,
943
- asset.filename,
944
- asset.metadata
945
- );
946
- await this.registerPhoto(staged.file_id);
947
- if (input.approveMedia) {
948
- if (!asset.metadata.rights_confirmed || asset.metadata.private_preview_only)
949
- throw new Error("Publication requires confirmed image rights");
950
- if (existing?.state !== "APPROVED") {
951
- current = await this.get(created.article_id);
952
- await this.command(created.article_id, current.generation, {
953
- action: "RESOLVE",
954
- contribution_id: staged.contribution_id,
955
- state: "APPROVED"
956
- });
957
- }
958
- selected.push({
959
- kind: "image",
960
- contribution_id: staged.contribution_id,
961
- file_id: staged.file_id,
962
- caption: asset.metadata.caption,
963
- credit: asset.metadata.credit,
964
- alt: asset.metadata.alt,
965
- position: asset.position
966
- });
967
- }
968
- }
969
- if (selected.length) {
970
- const current = await this.get(created.article_id);
971
- const document = { ...input.document, media: selected };
972
- const saved = current.revisions.find(
973
- (r) => r.id === current.current_revision_id
974
- )?.document;
975
- if (!saved) throw new Error("Current immutable revision is missing");
976
- if (sameDocument(saved, document))
977
- return {
978
- ...created,
979
- revision_id: current.current_revision_id,
980
- generation: current.generation,
981
- editorial_status: current.status,
982
- replayed: true
983
- };
984
- if (!sameDocument(
985
- { ...saved, media: [] },
986
- { ...input.document, media: [] }
987
- ) || saved.media?.length)
988
- throw new ArticleWorkflowError(
989
- "The article was edited after this package began; load its current revision before changing it",
990
- 409,
991
- created.article_id
992
- );
993
- return await this.save(
994
- `${input.operationKey}:assets`,
995
- created.article_id,
996
- current.generation,
997
- document,
998
- "MEDIA_UPDATE"
999
- );
1000
- }
1001
- return created;
1002
- } catch (error) {
1003
- if (error instanceof ArticleWorkflowError) {
1004
- error.articleId = created.article_id;
1005
- throw error;
1006
- }
1007
- throw new ArticleWorkflowError(
1008
- error instanceof Error ? error.message : "Article package interrupted",
1009
- 0,
1010
- created.article_id
1011
- );
1012
- }
903
+ /** One server-owned submission. No client-side Photo/TTS orchestration and no
904
+ * fallback to a different workflow. A 202 receipt is acceptance, not readiness.
905
+ * Replay the identical manifest/files/key after an uncertain acknowledgement.
906
+ */
907
+ submit(manifest, operationKey, files = [], signal) {
908
+ if (!operationKey) throw new Error("A stable operation key is required");
909
+ if (files.length > 10 || files.some((f) => f.file.size < 1 || f.file.size > 25 * 1024 * 1024) || files.reduce((n, f) => n + f.file.size, 0) > 50 * 1024 * 1024) throw new Error("Supply up to ten photographs, at most 25 MiB each and 50 MiB total");
910
+ const slots = new Set(files.map((f) => f.slot));
911
+ if (slots.size !== files.length || files.length !== (manifest.assets?.length ?? 0) || manifest.assets?.some((a) => !slots.has(a.slot))) throw new Error("Supply one file for each declared asset slot");
912
+ if (!files.length) return this.request("/submissions", "POST", manifest, { "Idempotency-Key": operationKey }, signal);
913
+ const form = new FormData();
914
+ form.append("manifest", JSON.stringify(manifest));
915
+ for (const asset of files) form.append(asset.slot, asset.file, asset.filename);
916
+ return this.request("/submissions", "POST", form, { "Idempotency-Key": operationKey }, signal);
917
+ }
918
+ submission(submissionId, signal) {
919
+ return this.request("/submissions/" + encodeURIComponent(submissionId), "GET", void 0, void 0, signal);
920
+ }
921
+ submitArticle(input) {
922
+ if (input.document.media?.length) throw new Error("Pass file assets separately when creating an article package");
923
+ const assets = (input.assets ?? []).map((a, i) => ({ ...a, slot: "asset_" + String.fromCharCode(97 + i) }));
924
+ return this.submit({
925
+ article: { operation: "create", author_profile_id: input.authorProfileId, document: input.document, review_policy: input.reviewPolicy },
926
+ intent: input.intent ?? "draft",
927
+ approve_media: input.approveMedia,
928
+ assets: assets.map((a) => ({ slot: a.slot, position: a.position, metadata: a.metadata }))
929
+ }, input.operationKey, assets);
1013
930
  }
1014
931
  };
1015
- function sameDocument(a, b) {
1016
- const sorted = (v) => Array.isArray(v) ? v.map(sorted) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])])) : v;
1017
- const canonical3 = (d) => [
1018
- d.title,
1019
- d.subtitle || "",
1020
- d.content_markdown,
1021
- d.excerpt ?? null,
1022
- sorted(d.cards ?? null),
1023
- sorted(d.citations ?? null),
1024
- d.section,
1025
- d.community || "",
1026
- d.format || null,
1027
- d.layout ? [d.layout.id, d.layout.version] : null,
1028
- (d.media || []).map((m) => [
1029
- m.kind || "image",
1030
- m.contribution_id,
1031
- m.file_id,
1032
- m.caption,
1033
- m.credit,
1034
- m.alt,
1035
- m.position
1036
- ])
1037
- ];
1038
- return JSON.stringify(canonical3(a)) === JSON.stringify(canonical3(b));
1039
- }
1040
932
 
1041
933
  exports.ArticleEditOperationError = ArticleEditOperationError;
1042
934
  exports.ArticlePhotoOperationError = ArticlePhotoOperationError;
@@ -112,13 +112,13 @@ type ArticleMedia = {
112
112
  position: "hero" | "inline" | "gallery" | "attachment";
113
113
  };
114
114
  type AssetMetadata = {
115
- caption: string;
116
- credit: string;
117
- alt: string;
118
- creator: string;
119
- who_is_pictured: string;
120
- when_taken: string;
121
- where_taken: string;
115
+ caption?: string;
116
+ credit?: string;
117
+ alt?: string;
118
+ creator?: string;
119
+ who_is_pictured?: string;
120
+ when_taken?: string;
121
+ where_taken?: string;
122
122
  rights_confirmed: boolean;
123
123
  private_preview_only?: boolean;
124
124
  };
@@ -195,6 +195,8 @@ type ArticleWorkflowOptions = {
195
195
  prioritized_narration_processing?: boolean;
196
196
  content_first_editor_document?: boolean;
197
197
  versioned_editorial_fields?: boolean;
198
+ managed_submission_version?: number | null;
199
+ multipart_submission?: boolean;
198
200
  };
199
201
  };
200
202
  type ArticleOperation = {
@@ -224,6 +226,56 @@ type Record = {
224
226
  };
225
227
  }[];
226
228
  };
229
+ type ArticleSubmissionManifest = {
230
+ article: {
231
+ operation: "create";
232
+ author_profile_id: string;
233
+ document: ArticleDocument;
234
+ review_policy?: string;
235
+ celebration_type?: string;
236
+ } | {
237
+ operation: "save";
238
+ article_id: string;
239
+ expected_generation: number;
240
+ document: ArticleDocument;
241
+ reason: string;
242
+ };
243
+ intent?: "draft" | "review" | "publish";
244
+ approve_media?: boolean;
245
+ sensitivity_review?: {
246
+ policy_version: string;
247
+ rule_ids: string[];
248
+ reason: string;
249
+ };
250
+ assets?: {
251
+ slot: string;
252
+ position: "hero" | "gallery" | "attachment";
253
+ metadata: AssetMetadata;
254
+ }[];
255
+ };
256
+ type ArticleSubmissionReceipt = {
257
+ submission_id: string;
258
+ article_id: string | null;
259
+ revision_id: string | null;
260
+ status: "intake" | "queued" | "scanning" | "narrating" | "awaiting_review" | "held" | "complete" | "published";
261
+ error_code: string | null;
262
+ status_path: string;
263
+ permalink_path: string | null;
264
+ processing: ArticleProcessing | null;
265
+ progress: {
266
+ intent: string;
267
+ assets: {
268
+ [slot: string]: {
269
+ state: string;
270
+ contribution_id?: string;
271
+ file_id?: string;
272
+ };
273
+ };
274
+ };
275
+ accepted_is_not_published: true;
276
+ public_ready: boolean;
277
+ next_action: string;
278
+ };
227
279
  declare class ArticleWorkflowError extends Error {
228
280
  status: number;
229
281
  articleId?: string | undefined;
@@ -303,22 +355,30 @@ declare class PublicationArticleClient {
303
355
  kind: "image";
304
356
  state: "OPEN";
305
357
  }>;
306
- /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
307
- * replay the same operation key to inspect/reuse staged contributions before another transfer.
308
- * Editorial approval and release are always separate explicit operations. */
358
+ /** One server-owned submission. No client-side Photo/TTS orchestration and no
359
+ * fallback to a different workflow. A 202 receipt is acceptance, not readiness.
360
+ * Replay the identical manifest/files/key after an uncertain acknowledgement.
361
+ */
362
+ submit(manifest: ArticleSubmissionManifest, operationKey: string, files?: {
363
+ slot: string;
364
+ file: Blob;
365
+ filename: string;
366
+ }[], signal?: AbortSignal): Promise<ArticleSubmissionReceipt>;
367
+ submission(submissionId: string, signal?: AbortSignal): Promise<ArticleSubmissionReceipt>;
309
368
  submitArticle(input: {
310
369
  operationKey: string;
311
370
  authorProfileId: string;
312
371
  document: ArticleDocument;
313
372
  reviewPolicy?: string;
373
+ intent?: "draft" | "review" | "publish";
314
374
  assets?: {
315
375
  file: Blob;
316
376
  filename: string;
317
377
  metadata: AssetMetadata;
318
- position: ArticleMedia["position"];
378
+ position: "hero" | "gallery" | "attachment";
319
379
  }[];
320
380
  approveMedia?: boolean;
321
- }): Promise<ArticleOperation>;
381
+ }): Promise<ArticleSubmissionReceipt>;
322
382
  }
323
383
 
324
- export { type ArticleDocument, type ArticleEditCheckpoint, type ArticleEditInput, ArticleEditOperationError, type ArticleEditPatch, type ArticleMedia, type ArticleOperation, type ArticlePhotoInput, ArticlePhotoOperationError, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, type PhotoOperationCheckpoint, PublicationArticleClient, type PublishedCoverReplacement, articleEditHtmlMatches, articlePhotoHtmlMatches };
384
+ export { type ArticleDocument, type ArticleEditCheckpoint, type ArticleEditInput, ArticleEditOperationError, type ArticleEditPatch, type ArticleMedia, type ArticleOperation, type ArticlePhotoInput, ArticlePhotoOperationError, type ArticleProcessing, type ArticleSubmissionManifest, type ArticleSubmissionReceipt, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, type PhotoOperationCheckpoint, PublicationArticleClient, type PublishedCoverReplacement, articleEditHtmlMatches, articlePhotoHtmlMatches };
@@ -112,13 +112,13 @@ type ArticleMedia = {
112
112
  position: "hero" | "inline" | "gallery" | "attachment";
113
113
  };
114
114
  type AssetMetadata = {
115
- caption: string;
116
- credit: string;
117
- alt: string;
118
- creator: string;
119
- who_is_pictured: string;
120
- when_taken: string;
121
- where_taken: string;
115
+ caption?: string;
116
+ credit?: string;
117
+ alt?: string;
118
+ creator?: string;
119
+ who_is_pictured?: string;
120
+ when_taken?: string;
121
+ where_taken?: string;
122
122
  rights_confirmed: boolean;
123
123
  private_preview_only?: boolean;
124
124
  };
@@ -195,6 +195,8 @@ type ArticleWorkflowOptions = {
195
195
  prioritized_narration_processing?: boolean;
196
196
  content_first_editor_document?: boolean;
197
197
  versioned_editorial_fields?: boolean;
198
+ managed_submission_version?: number | null;
199
+ multipart_submission?: boolean;
198
200
  };
199
201
  };
200
202
  type ArticleOperation = {
@@ -224,6 +226,56 @@ type Record = {
224
226
  };
225
227
  }[];
226
228
  };
229
+ type ArticleSubmissionManifest = {
230
+ article: {
231
+ operation: "create";
232
+ author_profile_id: string;
233
+ document: ArticleDocument;
234
+ review_policy?: string;
235
+ celebration_type?: string;
236
+ } | {
237
+ operation: "save";
238
+ article_id: string;
239
+ expected_generation: number;
240
+ document: ArticleDocument;
241
+ reason: string;
242
+ };
243
+ intent?: "draft" | "review" | "publish";
244
+ approve_media?: boolean;
245
+ sensitivity_review?: {
246
+ policy_version: string;
247
+ rule_ids: string[];
248
+ reason: string;
249
+ };
250
+ assets?: {
251
+ slot: string;
252
+ position: "hero" | "gallery" | "attachment";
253
+ metadata: AssetMetadata;
254
+ }[];
255
+ };
256
+ type ArticleSubmissionReceipt = {
257
+ submission_id: string;
258
+ article_id: string | null;
259
+ revision_id: string | null;
260
+ status: "intake" | "queued" | "scanning" | "narrating" | "awaiting_review" | "held" | "complete" | "published";
261
+ error_code: string | null;
262
+ status_path: string;
263
+ permalink_path: string | null;
264
+ processing: ArticleProcessing | null;
265
+ progress: {
266
+ intent: string;
267
+ assets: {
268
+ [slot: string]: {
269
+ state: string;
270
+ contribution_id?: string;
271
+ file_id?: string;
272
+ };
273
+ };
274
+ };
275
+ accepted_is_not_published: true;
276
+ public_ready: boolean;
277
+ next_action: string;
278
+ };
227
279
  declare class ArticleWorkflowError extends Error {
228
280
  status: number;
229
281
  articleId?: string | undefined;
@@ -303,22 +355,30 @@ declare class PublicationArticleClient {
303
355
  kind: "image";
304
356
  state: "OPEN";
305
357
  }>;
306
- /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
307
- * replay the same operation key to inspect/reuse staged contributions before another transfer.
308
- * Editorial approval and release are always separate explicit operations. */
358
+ /** One server-owned submission. No client-side Photo/TTS orchestration and no
359
+ * fallback to a different workflow. A 202 receipt is acceptance, not readiness.
360
+ * Replay the identical manifest/files/key after an uncertain acknowledgement.
361
+ */
362
+ submit(manifest: ArticleSubmissionManifest, operationKey: string, files?: {
363
+ slot: string;
364
+ file: Blob;
365
+ filename: string;
366
+ }[], signal?: AbortSignal): Promise<ArticleSubmissionReceipt>;
367
+ submission(submissionId: string, signal?: AbortSignal): Promise<ArticleSubmissionReceipt>;
309
368
  submitArticle(input: {
310
369
  operationKey: string;
311
370
  authorProfileId: string;
312
371
  document: ArticleDocument;
313
372
  reviewPolicy?: string;
373
+ intent?: "draft" | "review" | "publish";
314
374
  assets?: {
315
375
  file: Blob;
316
376
  filename: string;
317
377
  metadata: AssetMetadata;
318
- position: ArticleMedia["position"];
378
+ position: "hero" | "gallery" | "attachment";
319
379
  }[];
320
380
  approveMedia?: boolean;
321
- }): Promise<ArticleOperation>;
381
+ }): Promise<ArticleSubmissionReceipt>;
322
382
  }
323
383
 
324
- export { type ArticleDocument, type ArticleEditCheckpoint, type ArticleEditInput, ArticleEditOperationError, type ArticleEditPatch, type ArticleMedia, type ArticleOperation, type ArticlePhotoInput, ArticlePhotoOperationError, type ArticleProcessing, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, type PhotoOperationCheckpoint, PublicationArticleClient, type PublishedCoverReplacement, articleEditHtmlMatches, articlePhotoHtmlMatches };
384
+ export { type ArticleDocument, type ArticleEditCheckpoint, type ArticleEditInput, ArticleEditOperationError, type ArticleEditPatch, type ArticleMedia, type ArticleOperation, type ArticlePhotoInput, ArticlePhotoOperationError, type ArticleProcessing, type ArticleSubmissionManifest, type ArticleSubmissionReceipt, ArticleWorkflowError, type ArticleWorkflowOptions, type AssetMetadata, type PhotoOperationCheckpoint, PublicationArticleClient, type PublishedCoverReplacement, articleEditHtmlMatches, articlePhotoHtmlMatches };
@@ -898,142 +898,34 @@ var PublicationArticleClient = class {
898
898
  "If-Match": String(generation)
899
899
  });
900
900
  }
901
- /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
902
- * replay the same operation key to inspect/reuse staged contributions before another transfer.
903
- * Editorial approval and release are always separate explicit operations. */
904
- async submitArticle(input) {
905
- if (input.document.media?.length)
906
- throw new Error(
907
- "Pass file assets separately when creating an article package"
908
- );
909
- const capabilities = await this.options();
910
- if (capabilities.workflow_version !== 1)
911
- throw new Error("Unsupported article workflow version");
912
- if (input.assets?.length && !capabilities.capabilities.managed_private_asset_upload)
913
- throw new Error("Managed article upload is not deployed");
914
- const created = await this.create(
915
- input.operationKey,
916
- input.authorProfileId,
917
- input.document,
918
- input.reviewPolicy
919
- );
920
- try {
921
- const selected = [];
922
- for (const asset of input.assets ?? []) {
923
- let current = await this.get(created.article_id);
924
- const digest3 = Array.from(
925
- new Uint8Array(
926
- await crypto.subtle.digest(
927
- "SHA-256",
928
- await asset.file.arrayBuffer()
929
- )
930
- )
931
- ).map((v) => v.toString(16).padStart(2, "0")).join("");
932
- const existing = current.contributions.find(
933
- (c) => c.kind === "MEDIA" && c.state !== "REJECTED" && c.state !== "WITHDRAWN" && c.payload.sha256 === digest3 && Object.entries(asset.metadata).every(
934
- ([k, v]) => c.payload[k] === v
935
- )
936
- );
937
- const staged = existing?.payload.file_id ? { contribution_id: existing.id, file_id: existing.payload.file_id } : await this.upload(
938
- created.article_id,
939
- current.generation,
940
- asset.file,
941
- asset.filename,
942
- asset.metadata
943
- );
944
- await this.registerPhoto(staged.file_id);
945
- if (input.approveMedia) {
946
- if (!asset.metadata.rights_confirmed || asset.metadata.private_preview_only)
947
- throw new Error("Publication requires confirmed image rights");
948
- if (existing?.state !== "APPROVED") {
949
- current = await this.get(created.article_id);
950
- await this.command(created.article_id, current.generation, {
951
- action: "RESOLVE",
952
- contribution_id: staged.contribution_id,
953
- state: "APPROVED"
954
- });
955
- }
956
- selected.push({
957
- kind: "image",
958
- contribution_id: staged.contribution_id,
959
- file_id: staged.file_id,
960
- caption: asset.metadata.caption,
961
- credit: asset.metadata.credit,
962
- alt: asset.metadata.alt,
963
- position: asset.position
964
- });
965
- }
966
- }
967
- if (selected.length) {
968
- const current = await this.get(created.article_id);
969
- const document = { ...input.document, media: selected };
970
- const saved = current.revisions.find(
971
- (r) => r.id === current.current_revision_id
972
- )?.document;
973
- if (!saved) throw new Error("Current immutable revision is missing");
974
- if (sameDocument(saved, document))
975
- return {
976
- ...created,
977
- revision_id: current.current_revision_id,
978
- generation: current.generation,
979
- editorial_status: current.status,
980
- replayed: true
981
- };
982
- if (!sameDocument(
983
- { ...saved, media: [] },
984
- { ...input.document, media: [] }
985
- ) || saved.media?.length)
986
- throw new ArticleWorkflowError(
987
- "The article was edited after this package began; load its current revision before changing it",
988
- 409,
989
- created.article_id
990
- );
991
- return await this.save(
992
- `${input.operationKey}:assets`,
993
- created.article_id,
994
- current.generation,
995
- document,
996
- "MEDIA_UPDATE"
997
- );
998
- }
999
- return created;
1000
- } catch (error) {
1001
- if (error instanceof ArticleWorkflowError) {
1002
- error.articleId = created.article_id;
1003
- throw error;
1004
- }
1005
- throw new ArticleWorkflowError(
1006
- error instanceof Error ? error.message : "Article package interrupted",
1007
- 0,
1008
- created.article_id
1009
- );
1010
- }
901
+ /** One server-owned submission. No client-side Photo/TTS orchestration and no
902
+ * fallback to a different workflow. A 202 receipt is acceptance, not readiness.
903
+ * Replay the identical manifest/files/key after an uncertain acknowledgement.
904
+ */
905
+ submit(manifest, operationKey, files = [], signal) {
906
+ if (!operationKey) throw new Error("A stable operation key is required");
907
+ if (files.length > 10 || files.some((f) => f.file.size < 1 || f.file.size > 25 * 1024 * 1024) || files.reduce((n, f) => n + f.file.size, 0) > 50 * 1024 * 1024) throw new Error("Supply up to ten photographs, at most 25 MiB each and 50 MiB total");
908
+ const slots = new Set(files.map((f) => f.slot));
909
+ if (slots.size !== files.length || files.length !== (manifest.assets?.length ?? 0) || manifest.assets?.some((a) => !slots.has(a.slot))) throw new Error("Supply one file for each declared asset slot");
910
+ if (!files.length) return this.request("/submissions", "POST", manifest, { "Idempotency-Key": operationKey }, signal);
911
+ const form = new FormData();
912
+ form.append("manifest", JSON.stringify(manifest));
913
+ for (const asset of files) form.append(asset.slot, asset.file, asset.filename);
914
+ return this.request("/submissions", "POST", form, { "Idempotency-Key": operationKey }, signal);
915
+ }
916
+ submission(submissionId, signal) {
917
+ return this.request("/submissions/" + encodeURIComponent(submissionId), "GET", void 0, void 0, signal);
918
+ }
919
+ submitArticle(input) {
920
+ if (input.document.media?.length) throw new Error("Pass file assets separately when creating an article package");
921
+ const assets = (input.assets ?? []).map((a, i) => ({ ...a, slot: "asset_" + String.fromCharCode(97 + i) }));
922
+ return this.submit({
923
+ article: { operation: "create", author_profile_id: input.authorProfileId, document: input.document, review_policy: input.reviewPolicy },
924
+ intent: input.intent ?? "draft",
925
+ approve_media: input.approveMedia,
926
+ assets: assets.map((a) => ({ slot: a.slot, position: a.position, metadata: a.metadata }))
927
+ }, input.operationKey, assets);
1011
928
  }
1012
929
  };
1013
- function sameDocument(a, b) {
1014
- const sorted = (v) => Array.isArray(v) ? v.map(sorted) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])])) : v;
1015
- const canonical3 = (d) => [
1016
- d.title,
1017
- d.subtitle || "",
1018
- d.content_markdown,
1019
- d.excerpt ?? null,
1020
- sorted(d.cards ?? null),
1021
- sorted(d.citations ?? null),
1022
- d.section,
1023
- d.community || "",
1024
- d.format || null,
1025
- d.layout ? [d.layout.id, d.layout.version] : null,
1026
- (d.media || []).map((m) => [
1027
- m.kind || "image",
1028
- m.contribution_id,
1029
- m.file_id,
1030
- m.caption,
1031
- m.credit,
1032
- m.alt,
1033
- m.position
1034
- ])
1035
- ];
1036
- return JSON.stringify(canonical3(a)) === JSON.stringify(canonical3(b));
1037
- }
1038
930
 
1039
931
  export { ArticleEditOperationError, ArticlePhotoOperationError, ArticleWorkflowError, PublicationArticleClient, articleEditHtmlMatches, articlePhotoHtmlMatches };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.81",
3
+ "version": "0.0.82",
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": {