@scalemule/news-publication 0.0.80 → 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.
@@ -10,6 +10,47 @@ function articlePhotoHtmlMatches(html, lead, social) {
10
10
  return image && openGraph;
11
11
  }
12
12
 
13
+ // src/article-edit-verification.ts
14
+ var decode = (value) => value.replace(/&(?:amp|lt|gt|quot|apos|#39|#\d+|#x[\da-f]+);/giu, (s) => {
15
+ const named = {
16
+ "&": "&",
17
+ "&lt;": "<",
18
+ "&gt;": ">",
19
+ "&quot;": '"',
20
+ "&apos;": "'",
21
+ "&#39;": "'"
22
+ };
23
+ if (named[s.toLowerCase()]) return named[s.toLowerCase()];
24
+ const hex = s.toLowerCase().startsWith("&#x");
25
+ const n = parseInt(s.slice(hex ? 3 : 2, -1), hex ? 16 : 10);
26
+ return Number.isFinite(n) && n >= 0 && n <= 1114111 ? String.fromCodePoint(n) : s;
27
+ });
28
+ var text = (html) => decode(html.replace(/<[^>]*>/gu, " ")).replace(/\s+/gu, " ").trim();
29
+ var plain = (value) => value.replace(/\s+/gu, " ").trim();
30
+ function articleEditHtmlMatches(html, expected) {
31
+ const visible = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/giu, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/giu, " ");
32
+ const headline = [...visible.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/giu)].some(
33
+ (m) => text(m[1]) === plain(expected.title)
34
+ );
35
+ if (!headline) return false;
36
+ const paragraphs = [
37
+ ...expected.content_html.matchAll(
38
+ /<(p|li|h[2-6]|blockquote)\b[^>]*>([\s\S]*?)<\/\1>/giu
39
+ )
40
+ ].map((m) => text(m[2])).filter(Boolean);
41
+ const all = text(visible);
42
+ if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
43
+ return false;
44
+ const titles = [
45
+ ...visible.matchAll(
46
+ /<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
47
+ )
48
+ ].flatMap(
49
+ (m) => [...m[1].matchAll(/<h2\b[^>]*>([\s\S]*?)<\/h2>/giu)].map((m2) => text(m2[1]))
50
+ );
51
+ return JSON.stringify(titles.sort()) === JSON.stringify(expected.card_titles.map(plain).sort());
52
+ }
53
+
13
54
  // src/article-photo-operation.ts
14
55
  var ArticlePhotoOperationError = class extends Error {
15
56
  constructor(code, stage, message, fileId, status) {
@@ -285,6 +326,325 @@ async function runArticlePhoto(t, input) {
285
326
  }
286
327
  }
287
328
 
329
+ // src/article-edit-operation.ts
330
+ var ArticleEditOperationError = class extends Error {
331
+ constructor(code, stage, message, status, revisionId) {
332
+ super(message);
333
+ this.code = code;
334
+ this.stage = stage;
335
+ this.status = status;
336
+ this.revisionId = revisionId;
337
+ this.name = "ArticleEditOperationError";
338
+ }
339
+ code;
340
+ stage;
341
+ status;
342
+ revisionId;
343
+ };
344
+ var canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v === "object" ? Object.fromEntries(
345
+ Object.keys(v).sort().map((k) => [k, canonical2(v[k])])
346
+ ) : v;
347
+ var equal = (a, b) => JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
348
+ var digest2 = async (v) => [
349
+ ...new Uint8Array(
350
+ await crypto.subtle.digest(
351
+ "SHA-256",
352
+ new TextEncoder().encode(JSON.stringify(canonical2(v)))
353
+ )
354
+ )
355
+ ].map((v2) => v2.toString(16).padStart(2, "0")).join("");
356
+ var patchKeys = /* @__PURE__ */ new Set([
357
+ "title",
358
+ "subtitle",
359
+ "content_markdown",
360
+ "excerpt",
361
+ "section",
362
+ "community",
363
+ "format",
364
+ "layout",
365
+ "cards",
366
+ "citations"
367
+ ]);
368
+ function validate(input) {
369
+ if (!input.operationKey || !/^[\w.:-]{1,128}$/u.test(input.operationKey) || !input.articleId || !input.patch || typeof input.patch !== "object" || Array.isArray(input.patch) || !Object.keys(input.patch).length)
370
+ throw Error(
371
+ "Supply an article, stable operation key and nonempty editorial patch"
372
+ );
373
+ for (const [key, value] of Object.entries(input.patch)) {
374
+ if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
375
+ if (["cards", "citations"].includes(key)) {
376
+ if (!Array.isArray(value)) throw Error(key + " must be an array");
377
+ } else if (key === "layout") {
378
+ if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
379
+ throw Error("Select an existing versioned layout");
380
+ } else if (typeof value !== "string")
381
+ throw Error(key + " must be a string");
382
+ }
383
+ if (input.expectedGeneration !== void 0 && (!Number.isSafeInteger(input.expectedGeneration) || input.expectedGeneration < 1))
384
+ throw Error("Expected generation must be positive");
385
+ if (input.enableReviewPolicy && ![
386
+ "NONE",
387
+ "FACT_CHECK",
388
+ "CONTRIBUTOR_REVIEW",
389
+ "SUBJECT_APPROVAL_REQUIRED",
390
+ "SUBJECT_APPROVAL_PLUS_EDITOR"
391
+ ].includes(input.enableReviewPolicy))
392
+ throw Error("Select an explicit supported review policy");
393
+ if (new TextEncoder().encode(JSON.stringify(input.patch)).length > 128 * 1024)
394
+ throw Error("Editorial patch exceeds 128 KiB");
395
+ }
396
+ async function runArticleEdit(t, input) {
397
+ validate(input);
398
+ const started = performance.now();
399
+ let stage = "preflight";
400
+ const timeout = input.timeoutMs ?? 1e4;
401
+ if (!Number.isFinite(timeout) || timeout < 100 || timeout > 12e4)
402
+ throw Error("Edit deadline must be between 100 and 120000 milliseconds");
403
+ const signal = AbortSignal.timeout(timeout);
404
+ const request = (path, method, body, extra) => t.request(path, method, body, extra, signal);
405
+ const identity = await digest2([
406
+ t.publicationId,
407
+ input.articleId,
408
+ input.operationKey,
409
+ input.patch,
410
+ input.expectedGeneration ?? null,
411
+ input.enableReviewPolicy ?? null
412
+ ]);
413
+ if (input.checkpoint && (input.checkpoint.version !== 1 || input.checkpoint.identity !== identity || input.checkpoint.articleId !== input.articleId))
414
+ throw new ArticleEditOperationError(
415
+ "OPERATION_CONFLICT",
416
+ "validation",
417
+ "This checkpoint belongs to different input; reuse its original patch and operation key"
418
+ );
419
+ let state = input.checkpoint ? structuredClone(input.checkpoint) : { version: 1, identity, articleId: input.articleId, stage };
420
+ const save = async (next) => {
421
+ stage = next;
422
+ state.stage = next;
423
+ await input.saveCheckpoint(structuredClone(state));
424
+ };
425
+ const article = "/articles/" + encodeURIComponent(input.articleId);
426
+ const currentRevision = (record) => record.revisions?.find((v) => v.id === record.current_revision_id);
427
+ const identityOf = (post) => ({
428
+ slug: post.slug,
429
+ published_at: post.published_at ?? null,
430
+ canonical_url: post.canonical_url ?? null
431
+ });
432
+ const identityMatches = (post) => !state.sourceIdentity || state.sourceIdentity.slug === post.slug && (state.sourceIdentity.published_at === null || state.sourceIdentity.published_at === (post.published_at ?? null)) && (state.sourceIdentity.canonical_url === null || state.sourceIdentity.canonical_url === (post.canonical_url ?? null));
433
+ const result = (record, status) => ({
434
+ article_id: input.articleId,
435
+ revision_id: state.revisionId ?? record.current_revision_id,
436
+ generation: record.generation,
437
+ state: status,
438
+ editorial_status: record.status,
439
+ elapsed_ms: Math.round(performance.now() - started)
440
+ });
441
+ try {
442
+ const [options, source, initial] = await Promise.all([
443
+ request(
444
+ "/article-workflow/options?article_id=" + encodeURIComponent(input.articleId)
445
+ ),
446
+ request(article + "/document"),
447
+ request(article + "/collaboration").catch((error) => {
448
+ if (error.status === 404 && error.message === "Story collaboration is not enabled")
449
+ return null;
450
+ throw error;
451
+ })
452
+ ]);
453
+ if (options.workflow_version !== 1 || options.capabilities?.versioned_editorial_fields !== true)
454
+ throw new ArticleEditOperationError(
455
+ "CAPABILITY_UNAVAILABLE",
456
+ stage,
457
+ "The server must expose versioned_editorial_fields before this edit; no article was changed"
458
+ );
459
+ if (!identityMatches(source))
460
+ throw new ArticleEditOperationError(
461
+ "ARTICLE_IDENTITY_CHANGED",
462
+ stage,
463
+ "Article URL, publication date or canonical identity changed; inspect before continuing"
464
+ );
465
+ state.sourceIdentity ??= identityOf(source);
466
+ let record = initial;
467
+ if (!record) {
468
+ if (!input.enableReviewPolicy)
469
+ throw new ArticleEditOperationError(
470
+ "ENROLLMENT_REQUIRED",
471
+ stage,
472
+ "Legacy article: choose an explicit review policy for versioned editorial onboarding; no content was changed"
473
+ );
474
+ if (source.status === "scheduled")
475
+ throw new ArticleEditOperationError(
476
+ "SCHEDULED_ARTICLE",
477
+ stage,
478
+ "Onboarding cancels an old schedule; handle this scheduled article explicitly"
479
+ );
480
+ await save("enroll");
481
+ await request(article + "/collaboration/enable", "POST", {
482
+ review_policy: input.enableReviewPolicy
483
+ });
484
+ record = await request(article + "/collaboration");
485
+ }
486
+ if (state.revisionId) {
487
+ if (record.current_revision_id !== state.revisionId)
488
+ throw new ArticleEditOperationError(
489
+ "ARTICLE_CHANGED",
490
+ stage,
491
+ "Another revision is current; this operation cannot release or overwrite it",
492
+ 409,
493
+ state.revisionId
494
+ );
495
+ const selected = currentRevision(record);
496
+ if (!selected || !equal(selected.document, state.document))
497
+ throw new ArticleEditOperationError(
498
+ "VERIFICATION_CONFLICT",
499
+ stage,
500
+ "The saved revision differs from this operation",
501
+ 409,
502
+ state.revisionId
503
+ );
504
+ } else {
505
+ if (!state.document) {
506
+ if (input.expectedGeneration !== void 0 && record.generation !== input.expectedGeneration)
507
+ throw new ArticleEditOperationError(
508
+ "ARTICLE_CHANGED",
509
+ stage,
510
+ "The article generation changed; reload before applying this patch",
511
+ 409
512
+ );
513
+ const selected = currentRevision(record);
514
+ if (!selected?.complete_snapshot && selected?.complete_snapshot !== void 0)
515
+ throw new ArticleEditOperationError(
516
+ "INCOMPLETE_REVISION",
517
+ stage,
518
+ "Inspect this historical revision before editing"
519
+ );
520
+ if (!selected?.document)
521
+ throw Error("Current immutable article document is missing");
522
+ const effective = { ...selected.document };
523
+ for (const key of ["cards", "citations"])
524
+ if (effective[key] === void 0 && selected.editorial_metadata?.[key] !== void 0)
525
+ effective[key] = selected.editorial_metadata[key];
526
+ if (effective.excerpt === void 0 && source.excerpt !== null && source.excerpt !== void 0)
527
+ effective.excerpt = source.excerpt;
528
+ const changed = Object.entries(input.patch).some(
529
+ ([key, value]) => !equal(effective[key], value)
530
+ );
531
+ if (!changed) {
532
+ state.document = selected.document;
533
+ state.revisionId = record.current_revision_id;
534
+ state.savedGeneration = record.generation;
535
+ state.unchanged = true;
536
+ await save("verify");
537
+ if (!input.release) return result(record, "unchanged");
538
+ } else {
539
+ const document = { ...selected.document, ...input.patch };
540
+ if (document.excerpt === void 0 && source.excerpt !== null && source.excerpt !== void 0)
541
+ document.excerpt = source.excerpt;
542
+ state.document = document;
543
+ state.expectedGeneration = record.generation;
544
+ await save("save");
545
+ }
546
+ }
547
+ if (!state.revisionId) {
548
+ const saved = await request(
549
+ "/article-workflow",
550
+ "POST",
551
+ {
552
+ operation: "save",
553
+ article_id: input.articleId,
554
+ expected_generation: state.expectedGeneration,
555
+ document: state.document,
556
+ reason: state.sourceIdentity?.published_at ? "POST_PUBLICATION_CORRECTION" : "EDITOR_EDIT"
557
+ },
558
+ { "Idempotency-Key": input.operationKey }
559
+ );
560
+ state.revisionId = saved.revision_id;
561
+ state.savedGeneration = saved.generation;
562
+ await save("verify");
563
+ record = await request(article + "/collaboration");
564
+ if (record.current_revision_id !== state.revisionId || !equal(currentRevision(record)?.document, state.document))
565
+ throw new ArticleEditOperationError(
566
+ "ARTICLE_CHANGED",
567
+ stage,
568
+ "A different revision is current; no release was attempted",
569
+ 409,
570
+ state.revisionId
571
+ );
572
+ }
573
+ }
574
+ if (record.published_revision_id === state.revisionId) {
575
+ await save("published");
576
+ return result(record, "already_published");
577
+ }
578
+ if (!input.release) {
579
+ await save("saved");
580
+ return result(record, state.unchanged ? "unchanged" : "saved_for_review");
581
+ }
582
+ await save("release");
583
+ try {
584
+ await request(article + "/collaboration", "POST", {
585
+ action: "RELEASE",
586
+ expected_generation: record.generation,
587
+ scheduled_at: null
588
+ });
589
+ } catch (error) {
590
+ const checked = await request(article + "/collaboration");
591
+ if (checked.published_revision_id === state.revisionId && checked.current_revision_id === state.revisionId) {
592
+ record = checked;
593
+ } else throw error;
594
+ }
595
+ record = await request(article + "/collaboration");
596
+ if (record.current_revision_id !== state.revisionId || record.published_revision_id !== state.revisionId)
597
+ throw new ArticleEditOperationError(
598
+ "VERIFICATION_CONFLICT",
599
+ stage,
600
+ "This exact revision has not been confirmed published",
601
+ 409,
602
+ state.revisionId
603
+ );
604
+ const after = await request(article + "/document");
605
+ if (!identityMatches(after))
606
+ throw new ArticleEditOperationError(
607
+ "ARTICLE_IDENTITY_CHANGED",
608
+ stage,
609
+ "The release changed a permanent article identity",
610
+ 409,
611
+ state.revisionId
612
+ );
613
+ for (const key of ["title", "subtitle", "content_markdown", "excerpt"])
614
+ if (state.document?.[key] !== void 0 && !equal(after[key] ?? "", state.document[key]))
615
+ throw new ArticleEditOperationError(
616
+ "VERIFICATION_CONFLICT",
617
+ stage,
618
+ "Public article field differs after release: " + key,
619
+ 409,
620
+ state.revisionId
621
+ );
622
+ const normalizedCards = state.document?.cards;
623
+ if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
624
+ throw new ArticleEditOperationError(
625
+ "VERIFICATION_CONFLICT",
626
+ stage,
627
+ "Public card count differs from the saved revision",
628
+ 409,
629
+ state.revisionId
630
+ );
631
+ await save("published");
632
+ return result(record, "published");
633
+ } catch (error) {
634
+ if (error instanceof ArticleEditOperationError) throw error;
635
+ const status = error?.status;
636
+ const message = error instanceof Error ? error.message : "Article edit failed";
637
+ const code = signal.aborted ? "DEADLINE_EXCEEDED" : status === 401 || status === 403 ? "PERMISSION_DENIED" : status === 409 && stage === "release" ? "RELEASE_HELD" : status === 409 ? "ARTICLE_CHANGED" : "STEP_FAILED";
638
+ throw new ArticleEditOperationError(
639
+ code,
640
+ stage,
641
+ signal.aborted ? "Edit reached its deadline; resume the same patch, operation key and checkpoint. Success was not asserted." : message,
642
+ status,
643
+ state.revisionId
644
+ );
645
+ }
646
+ }
647
+
288
648
  // src/article-workflow-client.ts
289
649
  var ArticleWorkflowError = class extends Error {
290
650
  constructor(message, status, articleId) {
@@ -341,6 +701,9 @@ var PublicationArticleClient = class {
341
701
  `/article-workflow/options${articleId ? `?article_id=${encodeURIComponent(articleId)}` : ""}`
342
702
  );
343
703
  }
704
+ publicArticle(slug, signal) {
705
+ return this.request("/article-content/" + encodeURIComponent(slug), void 0, void 0, void 0, signal);
706
+ }
344
707
  /** Explicit approver-authorized onboarding for a legacy article; never called by get().
345
708
  * Existing schedules are cancelled by the server; public content remains unchanged.
346
709
  * Missing immutable media identities must be resolved through newsroom media controls.
@@ -369,6 +732,10 @@ var PublicationArticleClient = class {
369
732
  uploadAndReplacePublishedCover(input) {
370
733
  return runArticlePhoto({ publicationId: this.config.publicationId, apiUrl: this.config.apiUrl, fetch: this.config.fetch ?? fetch, request: this.request.bind(this) }, input);
371
734
  }
735
+ /** Save one exact revision; release is explicit and never manufactures approval. */
736
+ editArticle(input) {
737
+ return runArticleEdit({ publicationId: this.config.publicationId, request: this.request.bind(this) }, input);
738
+ }
372
739
  /** Replace a legacy publication cover using ready, measured public Photo renditions.
373
740
  * This never enrolls a story, publishes text, creates narration or retries a conflict.
374
741
  * Collaboration-managed stories retain their server-enforced immutable revision workflow.
@@ -531,138 +898,34 @@ var PublicationArticleClient = class {
531
898
  "If-Match": String(generation)
532
899
  });
533
900
  }
534
- /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
535
- * replay the same operation key to inspect/reuse staged contributions before another transfer.
536
- * Editorial approval and release are always separate explicit operations. */
537
- async submitArticle(input) {
538
- if (input.document.media?.length)
539
- throw new Error(
540
- "Pass file assets separately when creating an article package"
541
- );
542
- const capabilities = await this.options();
543
- if (capabilities.workflow_version !== 1)
544
- throw new Error("Unsupported article workflow version");
545
- if (input.assets?.length && !capabilities.capabilities.managed_private_asset_upload)
546
- throw new Error("Managed article upload is not deployed");
547
- const created = await this.create(
548
- input.operationKey,
549
- input.authorProfileId,
550
- input.document,
551
- input.reviewPolicy
552
- );
553
- try {
554
- const selected = [];
555
- for (const asset of input.assets ?? []) {
556
- let current = await this.get(created.article_id);
557
- const digest2 = Array.from(
558
- new Uint8Array(
559
- await crypto.subtle.digest(
560
- "SHA-256",
561
- await asset.file.arrayBuffer()
562
- )
563
- )
564
- ).map((v) => v.toString(16).padStart(2, "0")).join("");
565
- const existing = current.contributions.find(
566
- (c) => c.kind === "MEDIA" && c.state !== "REJECTED" && c.state !== "WITHDRAWN" && c.payload.sha256 === digest2 && Object.entries(asset.metadata).every(
567
- ([k, v]) => c.payload[k] === v
568
- )
569
- );
570
- const staged = existing?.payload.file_id ? { contribution_id: existing.id, file_id: existing.payload.file_id } : await this.upload(
571
- created.article_id,
572
- current.generation,
573
- asset.file,
574
- asset.filename,
575
- asset.metadata
576
- );
577
- await this.registerPhoto(staged.file_id);
578
- if (input.approveMedia) {
579
- if (!asset.metadata.rights_confirmed || asset.metadata.private_preview_only)
580
- throw new Error("Publication requires confirmed image rights");
581
- if (existing?.state !== "APPROVED") {
582
- current = await this.get(created.article_id);
583
- await this.command(created.article_id, current.generation, {
584
- action: "RESOLVE",
585
- contribution_id: staged.contribution_id,
586
- state: "APPROVED"
587
- });
588
- }
589
- selected.push({
590
- kind: "image",
591
- contribution_id: staged.contribution_id,
592
- file_id: staged.file_id,
593
- caption: asset.metadata.caption,
594
- credit: asset.metadata.credit,
595
- alt: asset.metadata.alt,
596
- position: asset.position
597
- });
598
- }
599
- }
600
- if (selected.length) {
601
- const current = await this.get(created.article_id);
602
- const document = { ...input.document, media: selected };
603
- const saved = current.revisions.find(
604
- (r) => r.id === current.current_revision_id
605
- )?.document;
606
- if (!saved) throw new Error("Current immutable revision is missing");
607
- if (sameDocument(saved, document))
608
- return {
609
- ...created,
610
- revision_id: current.current_revision_id,
611
- generation: current.generation,
612
- editorial_status: current.status,
613
- replayed: true
614
- };
615
- if (!sameDocument(
616
- { ...saved, media: [] },
617
- { ...input.document, media: [] }
618
- ) || saved.media?.length)
619
- throw new ArticleWorkflowError(
620
- "The article was edited after this package began; load its current revision before changing it",
621
- 409,
622
- created.article_id
623
- );
624
- return await this.save(
625
- `${input.operationKey}:assets`,
626
- created.article_id,
627
- current.generation,
628
- document,
629
- "MEDIA_UPDATE"
630
- );
631
- }
632
- return created;
633
- } catch (error) {
634
- if (error instanceof ArticleWorkflowError) {
635
- error.articleId = created.article_id;
636
- throw error;
637
- }
638
- throw new ArticleWorkflowError(
639
- error instanceof Error ? error.message : "Article package interrupted",
640
- 0,
641
- created.article_id
642
- );
643
- }
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);
644
928
  }
645
929
  };
646
- function sameDocument(a, b) {
647
- const canonical2 = (d) => [
648
- d.title,
649
- d.subtitle || "",
650
- d.content_markdown,
651
- d.section,
652
- d.community || "",
653
- d.format || null,
654
- d.layout ? [d.layout.id, d.layout.version] : null,
655
- (d.media || []).map((m) => [
656
- m.kind || "image",
657
- m.contribution_id,
658
- m.file_id,
659
- m.caption,
660
- m.credit,
661
- m.alt,
662
- m.position
663
- ])
664
- ];
665
- return JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
666
- }
667
930
 
668
- export { ArticlePhotoOperationError, ArticleWorkflowError, PublicationArticleClient, articlePhotoHtmlMatches };
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.80",
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": {