dshmarket 1.71.0 → 1.72.0

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/lib/http.js CHANGED
@@ -147,7 +147,10 @@ export function trustedAuthority(host, trustedHosts) {
147
147
  * - a cross-site page is refused — its Origin is present and different;
148
148
  * - `Origin: null` (a sandboxed iframe, a `data:` document) is refused, because
149
149
  * `null` is a present-but-unparseable origin, not an absent one;
150
- * - a same-host-but-different-port request is refused;
150
+ * - a same-host-but-different-port request is refused, except that a loopback
151
+ * Host accepts a port-less same-hostname Origin (#1031: Chrome 151+ omits
152
+ * the port on loopback Origins, and the dsh core fence follows the same
153
+ * rule);
151
154
  * - an EMPTY Origin is refused too: the header being present and unparseable
152
155
  * is not the same statement as its being absent, and only absence is what
153
156
  * a stripping proxy produces.
@@ -202,6 +205,16 @@ export function sameOrigin(request) {
202
205
  // equality cannot: a cross-site request whose Origin happens to match.
203
206
  if (request.headers['sec-fetch-site'] === 'cross-site')
204
207
  return false;
208
+ // Chrome 151+ serializes a loopback Origin WITHOUT its explicit port
209
+ // (#1031): the page at `http://127.0.0.1:3080` now states `Origin:
210
+ // http://127.0.0.1` on its same-origin POSTs, so the strict `host` equality
211
+ // below refuses installs and updates from the newest browsers. Mirror the
212
+ // dsh core rule (`isSameAuthorityOrigin`): the hostname must match, and the
213
+ // omitted port is honoured only on the loopback fence — `loopbackAuthority`
214
+ // above has already decided the Host names loopback, a cross-site page still
215
+ // fails the hostname test, and a rebinding page still cannot get its own
216
+ // name through both. Declared LAN names and proxies keep needing the exact
217
+ // port, as before.
205
218
  const origin = request.headers.origin;
206
219
  // A missing Origin is not a cross-site request: browsers send it on every
207
220
  // POST, same-origin included, so its absence means the caller is not a
@@ -211,7 +224,14 @@ export function sameOrigin(request) {
211
224
  if (origin === undefined)
212
225
  return true;
213
226
  try {
214
- return new URL(origin).host === host;
227
+ const originUrl = new URL(origin);
228
+ if (originUrl.host === host)
229
+ return true;
230
+ if (originUrl.port === '' && host !== undefined && loopbackAuthority(host)) {
231
+ const hostUrl = parseAuthority(host);
232
+ return hostUrl !== null && originUrl.hostname === hostUrl.hostname;
233
+ }
234
+ return false;
215
235
  }
216
236
  catch {
217
237
  return false;
@@ -77,7 +77,10 @@ export declare function trustedAuthority(host: string, trustedHosts: readonly st
77
77
  * - a cross-site page is refused — its Origin is present and different;
78
78
  * - `Origin: null` (a sandboxed iframe, a `data:` document) is refused, because
79
79
  * `null` is a present-but-unparseable origin, not an absent one;
80
- * - a same-host-but-different-port request is refused;
80
+ * - a same-host-but-different-port request is refused, except that a loopback
81
+ * Host accepts a port-less same-hostname Origin (#1031: Chrome 151+ omits
82
+ * the port on loopback Origins, and the dsh core fence follows the same
83
+ * rule);
81
84
  * - an EMPTY Origin is refused too: the header being present and unparseable
82
85
  * is not the same statement as its being absent, and only absence is what
83
86
  * a stripping proxy produces.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dshmarket",
3
3
  "description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
4
- "version": "1.71.0",
4
+ "version": "1.72.0",
5
5
  "icon": "assets/logo.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -3424,15 +3424,23 @@ export function MarketSection(props: MarketSectionProps) {
3424
3424
  // pluginBlocked closes over the installed map and the catalog match.
3425
3425
  // eslint-disable-next-line react-hooks/exhaustive-deps
3426
3426
  [pluginsAll, blockedNameSet, installed, repoIdentities, data, repoHints])
3427
+ // A sort the user chose is a request to see the list in THAT order, and
3428
+ // this block ignores the sort by design (it is "what was added in the last
3429
+ // seven days", ordered by downloads). Left in place it sits between the
3430
+ // control and the list it just reordered: on a 1166px window it fills the
3431
+ // screen, so switching to Stars looked like it did nothing (#1016). The
3432
+ // block is a front-page teaser for the unfiltered default view — one more
3433
+ // narrowing, alongside query, category and time range.
3427
3434
  const recentlyAdded = useMemo(
3428
3435
  () => data === null || tab !== 'discover' || q.trim() !== '' || cat !== 'all' || timeRange !== 'all'
3436
+ || sortField !== 'downloads' || sortDir !== 'desc'
3429
3437
  ? []
3430
3438
  : visiblePlugins(data.plugins, {
3431
3439
  category: 'all', query: '', lang, sort: 'downloads-desc', sinceDays: 7,
3432
3440
  hostCompatibility, compatibleWithHost,
3433
3441
  }).filter(plugin => !pluginBlocked(plugin)),
3434
3442
  // eslint-disable-next-line react-hooks/exhaustive-deps
3435
- [data, tab, q, cat, timeRange, lang, hostCompatibility, compatibleWithHost, blockedNameSet, installed, repoIdentities, repoHints])
3443
+ [data, tab, q, cat, timeRange, sortField, sortDir, lang, hostCompatibility, compatibleWithHost, blockedNameSet, installed, repoIdentities, repoHints])
3436
3444
  const recentRowRef = useRef<HTMLDivElement>(null)
3437
3445
  // Two per row, like the grid below it: three across read as a different
3438
3446
  // list rather than the same one (and left an odd one out at any width).
@@ -9,7 +9,7 @@ import { createElement as h } from 'react'
9
9
  import type { ReactNode } from 'react'
10
10
  import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
11
11
  import { missingIcons } from './icons.ts'
12
- import { en, zh } from './locales.ts'
12
+ import { en, ja, zh } from './locales.ts'
13
13
  import { InstallToast } from './InstallToast.tsx'
14
14
  import { MarketErrorBoundary } from './ErrorBoundary.tsx'
15
15
  import { MarketSection } from './MarketSection.tsx'
@@ -75,7 +75,7 @@ interface ThemeService {
75
75
 
76
76
  /** The subset of the locale service this plugin touches. */
77
77
  interface LocaleService {
78
- register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown
78
+ register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string>; ja?: Record<string, string> }): unknown
79
79
  bind(namespace: string): Translate
80
80
  subscribe(callback: () => void): () => void
81
81
  getSnapshot(): { active: string }
@@ -155,7 +155,7 @@ export function apply(ctx: MarketClientContext): void {
155
155
  * of them. Not exported: the split is a guard, not a surface.
156
156
  */
157
157
  function mountMarket(ctx: MarketClientContext): void {
158
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-market: dictionaries')
158
+ ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'dsh-market: dictionaries')
159
159
  const t = ctx.locale.bind(NS)
160
160
 
161
161
  // The section's nav glyph. The shell picks nav icons from its own built-in