@scalemule/news-publication 0.0.84 → 0.0.86
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
|
@@ -377,7 +377,7 @@ matches one declared slot. Credentials are normal project/member credentials.
|
|
|
377
377
|
The operation key is retained for 30 days. A legacy article must first have an
|
|
378
378
|
explicitly selected collaboration policy; this endpoint never downgrades review.
|
|
379
379
|
|
|
380
|
-
### Edit article prose, cards and
|
|
380
|
+
### Edit article prose, cards, citations and tags
|
|
381
381
|
|
|
382
382
|
```sh
|
|
383
383
|
npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
@@ -385,7 +385,10 @@ npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
The patch may contain title, subtitle, content_markdown, excerpt, section,
|
|
388
|
-
community, format, layout, cards and
|
|
388
|
+
community, format, layout, cards, citations and `tag_ids`. Tag edits require SDK
|
|
389
|
+
0.0.85+ and the deployed `versioned_editorial_tags` capability. Supply up to 32
|
|
390
|
+
existing tenant-owned tag UUIDs; an empty array clears assignments. Create or
|
|
391
|
+
select tags using the ordinary Blog tags API first. It preserves omitted fields and
|
|
389
392
|
media and saves one immutable revision. Default: save for review. Add
|
|
390
393
|
`--release` only to request release through the existing policy; the command
|
|
391
394
|
never issues APPROVE or downgrades review. On a legacy article, explicitly choose
|
|
@@ -402,7 +405,7 @@ Unknown save/release responses reconcile against the original revision; another
|
|
|
402
405
|
current revision stops rather than being overwritten. Completed replay and no-op
|
|
403
406
|
edits add no revision. `RELEASE_HELD`/exit 2 preserves the saved revision while
|
|
404
407
|
review is pending. With `--url --release`, success requires the rendered
|
|
405
|
-
headline, prose
|
|
408
|
+
headline, prose, actual card components and requested canonical tag links; public visibility pending is also
|
|
406
409
|
exit 2. A saved revision and a rendered published correction are separate states.
|
|
407
410
|
|
|
408
411
|
Use the same client method, `client.editArticle`, for a caller-supplied durable
|
|
@@ -18,7 +18,7 @@ Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized mem
|
|
|
18
18
|
Platform admins may use their normal access token with --application-id ID.
|
|
19
19
|
Optional --api-url URL defaults to https://api.scalemule.com.
|
|
20
20
|
|
|
21
|
-
edit saves prose, excerpt, cards and
|
|
21
|
+
edit saves prose, excerpt, cards, citations and tenant-owned tag_ids in one immutable revision.
|
|
22
22
|
Unspecified fields and media are retained. Default: save for review.
|
|
23
23
|
--release explicitly requests release under the existing policy; it never issues
|
|
24
24
|
APPROVE or changes a policy. Legacy onboarding requires an explicit
|
|
@@ -157,11 +157,11 @@ try {
|
|
|
157
157
|
while(!verified){
|
|
158
158
|
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
|
|
159
159
|
const html=await response.text();
|
|
160
|
-
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title)});
|
|
160
|
+
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
|
|
161
161
|
if(!verified)await new Promise(r=>setTimeout(r,Math.min(300,remaining())));
|
|
162
162
|
}
|
|
163
163
|
}catch(error){reason=error.message;}
|
|
164
|
-
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
164
|
+
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards/tags remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
165
165
|
else console.log(JSON.stringify({...result,public_page_verified:true,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
166
166
|
}else{
|
|
167
167
|
console.log(JSON.stringify({...result,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
@@ -43,6 +43,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
43
43
|
const all = text(visible);
|
|
44
44
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
45
45
|
return false;
|
|
46
|
+
if (expected.tags?.length) {
|
|
47
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
48
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
51
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}))) return false;
|
|
56
|
+
}
|
|
46
57
|
const titles = [
|
|
47
58
|
...visible.matchAll(
|
|
48
59
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -347,6 +358,16 @@ var canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v ===
|
|
|
347
358
|
Object.keys(v).sort().map((k) => [k, canonical2(v[k])])
|
|
348
359
|
) : v;
|
|
349
360
|
var equal = (a, b) => JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
|
|
361
|
+
var uuidIdentity = (value) => {
|
|
362
|
+
if (typeof value !== "string" || !/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)) return value;
|
|
363
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
364
|
+
};
|
|
365
|
+
var comparableDocument = (document) => document && typeof document === "object" ? { ...document, ...Array.isArray(document.media) ? { media: document.media.map((media) => ({
|
|
366
|
+
...media,
|
|
367
|
+
contribution_id: uuidIdentity(media.contribution_id),
|
|
368
|
+
file_id: uuidIdentity(media.file_id)
|
|
369
|
+
})) } : {} } : document;
|
|
370
|
+
var documentsEqual = (a, b) => equal(comparableDocument(a), comparableDocument(b));
|
|
350
371
|
var digest2 = async (v) => [
|
|
351
372
|
...new Uint8Array(
|
|
352
373
|
await crypto.subtle.digest(
|
|
@@ -365,7 +386,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
365
386
|
"format",
|
|
366
387
|
"layout",
|
|
367
388
|
"cards",
|
|
368
|
-
"citations"
|
|
389
|
+
"citations",
|
|
390
|
+
"tag_ids"
|
|
369
391
|
]);
|
|
370
392
|
function validate(input) {
|
|
371
393
|
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)
|
|
@@ -374,7 +396,10 @@ function validate(input) {
|
|
|
374
396
|
);
|
|
375
397
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
376
398
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
377
|
-
if (
|
|
399
|
+
if (key === "tag_ids") {
|
|
400
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
401
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
402
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
378
403
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
379
404
|
} else if (key === "layout") {
|
|
380
405
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -458,6 +483,8 @@ async function runArticleEdit(t, input) {
|
|
|
458
483
|
stage,
|
|
459
484
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
460
485
|
);
|
|
486
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
487
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
461
488
|
if (!identityMatches(source))
|
|
462
489
|
throw new ArticleEditOperationError(
|
|
463
490
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -495,7 +522,7 @@ async function runArticleEdit(t, input) {
|
|
|
495
522
|
state.revisionId
|
|
496
523
|
);
|
|
497
524
|
const selected = currentRevision(record);
|
|
498
|
-
if (!selected || !
|
|
525
|
+
if (!selected || !documentsEqual(selected.document, state.document))
|
|
499
526
|
throw new ArticleEditOperationError(
|
|
500
527
|
"VERIFICATION_CONFLICT",
|
|
501
528
|
stage,
|
|
@@ -563,7 +590,7 @@ async function runArticleEdit(t, input) {
|
|
|
563
590
|
state.savedGeneration = saved.generation;
|
|
564
591
|
await save("verify");
|
|
565
592
|
record = await request(article + "/collaboration");
|
|
566
|
-
if (record.current_revision_id !== state.revisionId || !
|
|
593
|
+
if (record.current_revision_id !== state.revisionId || !documentsEqual(currentRevision(record)?.document, state.document))
|
|
567
594
|
throw new ArticleEditOperationError(
|
|
568
595
|
"ARTICLE_CHANGED",
|
|
569
596
|
stage,
|
|
@@ -621,6 +648,8 @@ async function runArticleEdit(t, input) {
|
|
|
621
648
|
409,
|
|
622
649
|
state.revisionId
|
|
623
650
|
);
|
|
651
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
652
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
624
653
|
const normalizedCards = state.document?.cards;
|
|
625
654
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
626
655
|
throw new ArticleEditOperationError(
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -41,6 +41,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
41
41
|
const all = text(visible);
|
|
42
42
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
43
43
|
return false;
|
|
44
|
+
if (expected.tags?.length) {
|
|
45
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
46
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
49
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}))) return false;
|
|
54
|
+
}
|
|
44
55
|
const titles = [
|
|
45
56
|
...visible.matchAll(
|
|
46
57
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -345,6 +356,16 @@ var canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v ===
|
|
|
345
356
|
Object.keys(v).sort().map((k) => [k, canonical2(v[k])])
|
|
346
357
|
) : v;
|
|
347
358
|
var equal = (a, b) => JSON.stringify(canonical2(a)) === JSON.stringify(canonical2(b));
|
|
359
|
+
var uuidIdentity = (value) => {
|
|
360
|
+
if (typeof value !== "string" || !/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)) return value;
|
|
361
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
362
|
+
};
|
|
363
|
+
var comparableDocument = (document) => document && typeof document === "object" ? { ...document, ...Array.isArray(document.media) ? { media: document.media.map((media) => ({
|
|
364
|
+
...media,
|
|
365
|
+
contribution_id: uuidIdentity(media.contribution_id),
|
|
366
|
+
file_id: uuidIdentity(media.file_id)
|
|
367
|
+
})) } : {} } : document;
|
|
368
|
+
var documentsEqual = (a, b) => equal(comparableDocument(a), comparableDocument(b));
|
|
348
369
|
var digest2 = async (v) => [
|
|
349
370
|
...new Uint8Array(
|
|
350
371
|
await crypto.subtle.digest(
|
|
@@ -363,7 +384,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
363
384
|
"format",
|
|
364
385
|
"layout",
|
|
365
386
|
"cards",
|
|
366
|
-
"citations"
|
|
387
|
+
"citations",
|
|
388
|
+
"tag_ids"
|
|
367
389
|
]);
|
|
368
390
|
function validate(input) {
|
|
369
391
|
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)
|
|
@@ -372,7 +394,10 @@ function validate(input) {
|
|
|
372
394
|
);
|
|
373
395
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
374
396
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
375
|
-
if (
|
|
397
|
+
if (key === "tag_ids") {
|
|
398
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
399
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
400
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
376
401
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
377
402
|
} else if (key === "layout") {
|
|
378
403
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -456,6 +481,8 @@ async function runArticleEdit(t, input) {
|
|
|
456
481
|
stage,
|
|
457
482
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
458
483
|
);
|
|
484
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
485
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
459
486
|
if (!identityMatches(source))
|
|
460
487
|
throw new ArticleEditOperationError(
|
|
461
488
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -493,7 +520,7 @@ async function runArticleEdit(t, input) {
|
|
|
493
520
|
state.revisionId
|
|
494
521
|
);
|
|
495
522
|
const selected = currentRevision(record);
|
|
496
|
-
if (!selected || !
|
|
523
|
+
if (!selected || !documentsEqual(selected.document, state.document))
|
|
497
524
|
throw new ArticleEditOperationError(
|
|
498
525
|
"VERIFICATION_CONFLICT",
|
|
499
526
|
stage,
|
|
@@ -561,7 +588,7 @@ async function runArticleEdit(t, input) {
|
|
|
561
588
|
state.savedGeneration = saved.generation;
|
|
562
589
|
await save("verify");
|
|
563
590
|
record = await request(article + "/collaboration");
|
|
564
|
-
if (record.current_revision_id !== state.revisionId || !
|
|
591
|
+
if (record.current_revision_id !== state.revisionId || !documentsEqual(currentRevision(record)?.document, state.document))
|
|
565
592
|
throw new ArticleEditOperationError(
|
|
566
593
|
"ARTICLE_CHANGED",
|
|
567
594
|
stage,
|
|
@@ -619,6 +646,8 @@ async function runArticleEdit(t, input) {
|
|
|
619
646
|
409,
|
|
620
647
|
state.revisionId
|
|
621
648
|
);
|
|
649
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
650
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
622
651
|
const normalizedCards = state.document?.cards;
|
|
623
652
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
624
653
|
throw new ArticleEditOperationError(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.86",
|
|
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": {
|