@solongate/proxy 0.82.27 → 0.82.29
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/dist/index.js +53 -24
- package/dist/tui/index.js +53 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8814,6 +8814,7 @@ function PoliciesPanel({ focused }) {
|
|
|
8814
8814
|
const activeQ = useLoader(() => api.policies.active());
|
|
8815
8815
|
const activeId = activeQ.data?.policy?.id ?? null;
|
|
8816
8816
|
const activeBy = activeQ.data?.matched_by;
|
|
8817
|
+
const { rows: panelRows } = usePanelSize();
|
|
8817
8818
|
const [view, setView] = useState3("list");
|
|
8818
8819
|
const [pi, setPi] = useState3(0);
|
|
8819
8820
|
const [ri, setRi] = useState3(0);
|
|
@@ -8933,7 +8934,12 @@ function PoliciesPanel({ focused }) {
|
|
|
8933
8934
|
setView("rule");
|
|
8934
8935
|
} else if (input === "D") {
|
|
8935
8936
|
setStatus("Dry-running\u2026");
|
|
8936
|
-
void api.policies.dryRun({ rules, mode }).then((res) => setStatus(
|
|
8937
|
+
void api.policies.dryRun({ rules, mode }).then((res) => setStatus(
|
|
8938
|
+
`dry-run: replayed ${res.evaluated} recent calls against ${rules.length} rules
|
|
8939
|
+
would allow ${res.would_allow} would deny ${res.would_deny}
|
|
8940
|
+
newly allowed ${res.newly_allowed} newly blocked ${res.newly_blocked} unchanged ${res.unchanged}` + (res.sample_newly_blocked?.length ? `
|
|
8941
|
+
now blocked e.g.: ${res.sample_newly_blocked.slice(0, 3).map((s) => truncate2(s.tool, 24)).join(", ")}` : "")
|
|
8942
|
+
)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
|
|
8937
8943
|
} else if (input === "s") void save();
|
|
8938
8944
|
else if (input === "x") discard();
|
|
8939
8945
|
return;
|
|
@@ -8960,6 +8966,12 @@ function PoliciesPanel({ focused }) {
|
|
|
8960
8966
|
);
|
|
8961
8967
|
if (view === "list") {
|
|
8962
8968
|
const activeName = activeId ? policies.find((p) => p.id === activeId)?.name ?? activeId : null;
|
|
8969
|
+
const lHead = 3 + (status ? 1 : 0);
|
|
8970
|
+
const lBudget = Math.max(3, panelRows - lHead);
|
|
8971
|
+
const lStart = Math.min(Math.max(0, pi - Math.floor(lBudget / 2)), Math.max(0, policies.length - lBudget));
|
|
8972
|
+
const lWin = policies.slice(lStart, lStart + lBudget);
|
|
8973
|
+
const lAbove = lStart;
|
|
8974
|
+
const lBelow = Math.max(0, policies.length - (lStart + lBudget));
|
|
8963
8975
|
return /* @__PURE__ */ jsx3(DataView, { loading: list6.loading && !list6.data, error: list6.error, empty: !!list6.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
8964
8976
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
8965
8977
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "active: " }),
|
|
@@ -8971,30 +8983,42 @@ function PoliciesPanel({ focused }) {
|
|
|
8971
8983
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: activeBy ? ` (${activeBy})` : "" })
|
|
8972
8984
|
] }) : /* @__PURE__ */ jsx3(Text3, { color: theme.bad, bold: true, children: "\u25CB NONE \u2014 nothing enforced, grants refused" })
|
|
8973
8985
|
] }),
|
|
8974
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ?
|
|
8975
|
-
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children:
|
|
8976
|
-
|
|
8977
|
-
/* @__PURE__ */ jsxs3(Text3, { color: theme.
|
|
8978
|
-
p.
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8986
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: focused ? `\u2191\u2193 select \xB7 enter open \xB7 a activate \xB7 x deactivate \xB7 ^R refresh${lAbove ? ` \xB7 \u25B2${lAbove}` : ""}${lBelow ? ` \xB7 \u25BC${lBelow}` : ""}` : "press \u2192 to browse" }),
|
|
8987
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: lWin.map((p, wi) => {
|
|
8988
|
+
const i = lStart + wi;
|
|
8989
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
|
|
8990
|
+
(i === pi ? "\u25B8 " : " ") + truncate2(p.name, 26).padEnd(27),
|
|
8991
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
8992
|
+
p.mode.padEnd(10),
|
|
8993
|
+
" ",
|
|
8994
|
+
p.rules.length,
|
|
8995
|
+
" rules"
|
|
8996
|
+
] }),
|
|
8997
|
+
p.id === activeId ? /* @__PURE__ */ jsx3(Text3, { color: theme.ok, bold: true, children: " \u25CF ACTIVE" }) : null
|
|
8998
|
+
] }, p.id);
|
|
8999
|
+
}) }),
|
|
8985
9000
|
status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
|
|
8986
9001
|
] }) });
|
|
8987
9002
|
}
|
|
8988
9003
|
const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
|
|
8989
9004
|
if (view === "rules") {
|
|
9005
|
+
const rStatusLines = status ? status.split("\n").length : 0;
|
|
9006
|
+
const rHead = 4 + rStatusLines;
|
|
9007
|
+
const rBudget = Math.max(3, panelRows - rHead);
|
|
9008
|
+
const rMaxStart = Math.max(0, rules.length - rBudget);
|
|
9009
|
+
const rStart = Math.min(Math.max(0, ri - Math.floor(rBudget / 2)), rMaxStart);
|
|
9010
|
+
const rWin = rules.slice(rStart, rStart + rBudget);
|
|
9011
|
+
const rAbove = rStart;
|
|
9012
|
+
const rBelow = Math.max(0, rules.length - (rStart + rBudget));
|
|
8990
9013
|
return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
8991
9014
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
8992
9015
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate2(selected?.name ?? "", 28) }),
|
|
8993
9016
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
|
|
8994
9017
|
/* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
|
|
9018
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: ` ${rules.length} rules` }),
|
|
8995
9019
|
dirtyTag
|
|
8996
9020
|
] }),
|
|
8997
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children:
|
|
9021
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: `\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 s save \xB7 \u2190 back${rAbove ? ` \xB7 \u25B2${rAbove}` : ""}${rBelow ? ` \xB7 \u25BC${rBelow}` : ""}` }),
|
|
8998
9022
|
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
|
|
8999
9023
|
Table,
|
|
9000
9024
|
{
|
|
@@ -9003,17 +9027,22 @@ function PoliciesPanel({ focused }) {
|
|
|
9003
9027
|
{ header: "ON", width: 2 },
|
|
9004
9028
|
{ header: "EFFECT", width: 7 },
|
|
9005
9029
|
{ header: "PRIO", width: 4 },
|
|
9006
|
-
{ header: "TOOL", width:
|
|
9007
|
-
{ header: "
|
|
9030
|
+
{ header: "TOOL", width: 12 },
|
|
9031
|
+
{ header: "DESCRIPTION", width: 30 },
|
|
9032
|
+
{ header: "CONSTRAINTS", width: 14 }
|
|
9008
9033
|
],
|
|
9009
|
-
rows:
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9034
|
+
rows: rWin.map((r, wi) => {
|
|
9035
|
+
const i = rStart + wi;
|
|
9036
|
+
return [
|
|
9037
|
+
{ value: i === ri ? "\u25B8" : "", color: theme.accentBright },
|
|
9038
|
+
{ value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
|
|
9039
|
+
{ value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
|
|
9040
|
+
{ value: String(r.priority), dim: true },
|
|
9041
|
+
{ value: truncate2(r.toolPattern, 12), color: theme.accent, dim: !r.enabled },
|
|
9042
|
+
{ value: truncate2(r.description || "-", 30), dim: !r.enabled },
|
|
9043
|
+
{ value: ruleSummary(r) || "-", dim: true }
|
|
9044
|
+
];
|
|
9045
|
+
})
|
|
9017
9046
|
}
|
|
9018
9047
|
) }),
|
|
9019
9048
|
rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
|
|
@@ -9024,7 +9053,7 @@ function PoliciesPanel({ focused }) {
|
|
|
9024
9053
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
9025
9054
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
9026
9055
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
|
|
9027
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children:
|
|
9056
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: rule?.id ?? "" }),
|
|
9028
9057
|
dirtyTag
|
|
9029
9058
|
] }),
|
|
9030
9059
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
|
package/dist/tui/index.js
CHANGED
|
@@ -1939,6 +1939,7 @@ function PoliciesPanel({ focused }) {
|
|
|
1939
1939
|
const activeQ = useLoader(() => api.policies.active());
|
|
1940
1940
|
const activeId = activeQ.data?.policy?.id ?? null;
|
|
1941
1941
|
const activeBy = activeQ.data?.matched_by;
|
|
1942
|
+
const { rows: panelRows } = usePanelSize();
|
|
1942
1943
|
const [view, setView] = useState3("list");
|
|
1943
1944
|
const [pi, setPi] = useState3(0);
|
|
1944
1945
|
const [ri, setRi] = useState3(0);
|
|
@@ -2058,7 +2059,12 @@ function PoliciesPanel({ focused }) {
|
|
|
2058
2059
|
setView("rule");
|
|
2059
2060
|
} else if (input === "D") {
|
|
2060
2061
|
setStatus("Dry-running\u2026");
|
|
2061
|
-
void api.policies.dryRun({ rules, mode }).then((res) => setStatus(
|
|
2062
|
+
void api.policies.dryRun({ rules, mode }).then((res) => setStatus(
|
|
2063
|
+
`dry-run: replayed ${res.evaluated} recent calls against ${rules.length} rules
|
|
2064
|
+
would allow ${res.would_allow} would deny ${res.would_deny}
|
|
2065
|
+
newly allowed ${res.newly_allowed} newly blocked ${res.newly_blocked} unchanged ${res.unchanged}` + (res.sample_newly_blocked?.length ? `
|
|
2066
|
+
now blocked e.g.: ${res.sample_newly_blocked.slice(0, 3).map((s) => truncate(s.tool, 24)).join(", ")}` : "")
|
|
2067
|
+
)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
|
|
2062
2068
|
} else if (input === "s") void save();
|
|
2063
2069
|
else if (input === "x") discard();
|
|
2064
2070
|
return;
|
|
@@ -2085,6 +2091,12 @@ function PoliciesPanel({ focused }) {
|
|
|
2085
2091
|
);
|
|
2086
2092
|
if (view === "list") {
|
|
2087
2093
|
const activeName = activeId ? policies.find((p) => p.id === activeId)?.name ?? activeId : null;
|
|
2094
|
+
const lHead = 3 + (status ? 1 : 0);
|
|
2095
|
+
const lBudget = Math.max(3, panelRows - lHead);
|
|
2096
|
+
const lStart = Math.min(Math.max(0, pi - Math.floor(lBudget / 2)), Math.max(0, policies.length - lBudget));
|
|
2097
|
+
const lWin = policies.slice(lStart, lStart + lBudget);
|
|
2098
|
+
const lAbove = lStart;
|
|
2099
|
+
const lBelow = Math.max(0, policies.length - (lStart + lBudget));
|
|
2088
2100
|
return /* @__PURE__ */ jsx3(DataView, { loading: list5.loading && !list5.data, error: list5.error, empty: !!list5.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2089
2101
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
2090
2102
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "active: " }),
|
|
@@ -2096,30 +2108,42 @@ function PoliciesPanel({ focused }) {
|
|
|
2096
2108
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: activeBy ? ` (${activeBy})` : "" })
|
|
2097
2109
|
] }) : /* @__PURE__ */ jsx3(Text3, { color: theme.bad, bold: true, children: "\u25CB NONE \u2014 nothing enforced, grants refused" })
|
|
2098
2110
|
] }),
|
|
2099
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ?
|
|
2100
|
-
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children:
|
|
2101
|
-
|
|
2102
|
-
/* @__PURE__ */ jsxs3(Text3, { color: theme.
|
|
2103
|
-
p.
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2111
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: focused ? `\u2191\u2193 select \xB7 enter open \xB7 a activate \xB7 x deactivate \xB7 ^R refresh${lAbove ? ` \xB7 \u25B2${lAbove}` : ""}${lBelow ? ` \xB7 \u25BC${lBelow}` : ""}` : "press \u2192 to browse" }),
|
|
2112
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: lWin.map((p, wi) => {
|
|
2113
|
+
const i = lStart + wi;
|
|
2114
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
|
|
2115
|
+
(i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
|
|
2116
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
2117
|
+
p.mode.padEnd(10),
|
|
2118
|
+
" ",
|
|
2119
|
+
p.rules.length,
|
|
2120
|
+
" rules"
|
|
2121
|
+
] }),
|
|
2122
|
+
p.id === activeId ? /* @__PURE__ */ jsx3(Text3, { color: theme.ok, bold: true, children: " \u25CF ACTIVE" }) : null
|
|
2123
|
+
] }, p.id);
|
|
2124
|
+
}) }),
|
|
2110
2125
|
status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
|
|
2111
2126
|
] }) });
|
|
2112
2127
|
}
|
|
2113
2128
|
const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
|
|
2114
2129
|
if (view === "rules") {
|
|
2130
|
+
const rStatusLines = status ? status.split("\n").length : 0;
|
|
2131
|
+
const rHead = 4 + rStatusLines;
|
|
2132
|
+
const rBudget = Math.max(3, panelRows - rHead);
|
|
2133
|
+
const rMaxStart = Math.max(0, rules.length - rBudget);
|
|
2134
|
+
const rStart = Math.min(Math.max(0, ri - Math.floor(rBudget / 2)), rMaxStart);
|
|
2135
|
+
const rWin = rules.slice(rStart, rStart + rBudget);
|
|
2136
|
+
const rAbove = rStart;
|
|
2137
|
+
const rBelow = Math.max(0, rules.length - (rStart + rBudget));
|
|
2115
2138
|
return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2116
2139
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
2117
2140
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
|
|
2118
2141
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
|
|
2119
2142
|
/* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
|
|
2143
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: ` ${rules.length} rules` }),
|
|
2120
2144
|
dirtyTag
|
|
2121
2145
|
] }),
|
|
2122
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children:
|
|
2146
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: `\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 s save \xB7 \u2190 back${rAbove ? ` \xB7 \u25B2${rAbove}` : ""}${rBelow ? ` \xB7 \u25BC${rBelow}` : ""}` }),
|
|
2123
2147
|
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
|
|
2124
2148
|
Table,
|
|
2125
2149
|
{
|
|
@@ -2128,17 +2152,22 @@ function PoliciesPanel({ focused }) {
|
|
|
2128
2152
|
{ header: "ON", width: 2 },
|
|
2129
2153
|
{ header: "EFFECT", width: 7 },
|
|
2130
2154
|
{ header: "PRIO", width: 4 },
|
|
2131
|
-
{ header: "TOOL", width:
|
|
2132
|
-
{ header: "
|
|
2155
|
+
{ header: "TOOL", width: 12 },
|
|
2156
|
+
{ header: "DESCRIPTION", width: 30 },
|
|
2157
|
+
{ header: "CONSTRAINTS", width: 14 }
|
|
2133
2158
|
],
|
|
2134
|
-
rows:
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2159
|
+
rows: rWin.map((r, wi) => {
|
|
2160
|
+
const i = rStart + wi;
|
|
2161
|
+
return [
|
|
2162
|
+
{ value: i === ri ? "\u25B8" : "", color: theme.accentBright },
|
|
2163
|
+
{ value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
|
|
2164
|
+
{ value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
|
|
2165
|
+
{ value: String(r.priority), dim: true },
|
|
2166
|
+
{ value: truncate(r.toolPattern, 12), color: theme.accent, dim: !r.enabled },
|
|
2167
|
+
{ value: truncate(r.description || "-", 30), dim: !r.enabled },
|
|
2168
|
+
{ value: ruleSummary(r) || "-", dim: true }
|
|
2169
|
+
];
|
|
2170
|
+
})
|
|
2142
2171
|
}
|
|
2143
2172
|
) }),
|
|
2144
2173
|
rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
|
|
@@ -2149,7 +2178,7 @@ function PoliciesPanel({ focused }) {
|
|
|
2149
2178
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2150
2179
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
2151
2180
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
|
|
2152
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children:
|
|
2181
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: rule?.id ?? "" }),
|
|
2153
2182
|
dirtyTag
|
|
2154
2183
|
] }),
|
|
2155
2184
|
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.29",
|
|
4
4
|
"description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|