dshmarket 1.66.12 → 1.66.13

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/UPDATE-API-V1.md CHANGED
@@ -78,6 +78,14 @@ short update-check cache.
78
78
 
79
79
  Mutation requests require the same-origin protection used by the Market UI.
80
80
 
81
+ Every endpoint here — reads included — also sits behind DSH's own browser
82
+ login, on any host that has one (`dsh web` from 0.1.2-alpha.2 on): a request
83
+ without the host's session cookie is answered `401`, and one from an untrusted
84
+ origin `403`, exactly as DSH answers for its own API (#603). A plugin UI running
85
+ in the DSH page sends that cookie with every same-origin `fetch`, so it needs no
86
+ change. A script or tool calling these endpoints from outside the browser is the
87
+ anonymous client this refuses; there is no separate token for it.
88
+
81
89
  ```http
82
90
  POST /dsh-market/api/v1/updates
83
91
  Content-Type: application/json
package/lib/http.js CHANGED
@@ -32,6 +32,39 @@ export function sendJson(response, status, payload) {
32
32
  * undeclared name is still refused.
33
33
  */
34
34
  let trustedHostsSource = () => [];
35
+ let requestGateSource = () => undefined;
36
+ /** Point the market at the host's request gate. Returns the previous source. */
37
+ export function setRequestGateSource(source) {
38
+ const previous = requestGateSource;
39
+ requestGateSource = source;
40
+ return previous;
41
+ }
42
+ /**
43
+ * Answer the request with the host's rejection, if it has one.
44
+ * @returns true when the request was refused and answered here.
45
+ */
46
+ export function refuseUnadmitted(request, response) {
47
+ const gate = requestGateSource();
48
+ if (gate === undefined)
49
+ return false;
50
+ let status;
51
+ try {
52
+ status = gate(request);
53
+ }
54
+ catch {
55
+ // A gate that throws has not admitted anything. Failing open here would
56
+ // turn a host bug into the exposure this exists to close.
57
+ status = 403;
58
+ }
59
+ if (status === undefined)
60
+ return false;
61
+ sendJson(response, status, {
62
+ error: status === 401
63
+ ? '需要先登录 DSH:请重新打开 dsh web 启动时打印的那个网址。 / DSH login required: reopen the URL dsh web printed at startup.'
64
+ : '这个请求来自不受信任的地址,已拒绝。 / This request came from an untrusted origin and was refused.',
65
+ });
66
+ return true;
67
+ }
35
68
  /** Point the fence at the host's declared authorities. Returns the previous source. */
36
69
  export function setTrustedHostsSource(source) {
37
70
  const previous = trustedHostsSource;
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, isAbsolute } from 'node:path';
6
6
  import { createDesktopPluginRuntime, setHostPackageManager } from './dsh-cli.js';
7
7
  import { createOfficialDesktopRuntime } from './official-desktop.js';
8
8
  import { isDshProfileName } from './profile.js';
9
- import { setTrustedHostsSource } from './http.js';
9
+ import { setRequestGateSource, setTrustedHostsSource } from './http.js';
10
10
  import { mountMarketRoutes } from './routes.js';
11
11
  import { installDesktopMarketSettings, installMarketSettings } from './settings.js';
12
12
  export const name = 'dsh-market';
@@ -51,6 +51,22 @@ function launchedProfile(context) {
51
51
  return undefined;
52
52
  return { name, dir };
53
53
  }
54
+ /**
55
+ * The launcher's `profileContext` in one line, for the exported log (#744).
56
+ *
57
+ * Only the facts that decide the branch: whether the service is there, and
58
+ * whether its shape is one `launchedProfile` accepts. Never the directory
59
+ * itself — the log already carries the profile path where it is needed.
60
+ */
61
+ export function describeProfileContext(value) {
62
+ if (value === undefined || value === null)
63
+ return 'absent';
64
+ const launched = launchedProfile(value);
65
+ if (launched !== undefined)
66
+ return `present (name ${launched.name})`;
67
+ const shape = value;
68
+ return `present but unusable (name: ${typeof shape.name}, dir: ${typeof shape.dir})`;
69
+ }
54
70
  /**
55
71
  * The package manager the launcher published for this profile, if any.
56
72
  *
@@ -129,9 +145,22 @@ export function useTrustedHosts(ctx) {
129
145
  ? connection.trustedHosts.filter((entry) => typeof entry === 'string')
130
146
  : [];
131
147
  });
148
+ // The same service carries the host's request gate (#603): its fence plus
149
+ // its browser login, the check its own API runs. Read per request for the
150
+ // same reason as the authorities above.
151
+ const previousGate = setRequestGateSource(() => {
152
+ const connection = ctx.get('connection');
153
+ const gate = connection?.requestRejection;
154
+ return typeof gate === 'function'
155
+ ? (request => gate.call(connection, request))
156
+ : undefined;
157
+ });
132
158
  // The setter returns the PREVIOUS source, not a restore closure — the same
133
159
  // contract as setBuildEnvSource in dsh-cli.ts.
134
- return () => { setTrustedHostsSource(previous); };
160
+ return () => {
161
+ setTrustedHostsSource(previous);
162
+ setRequestGateSource(previousGate);
163
+ };
135
164
  }
136
165
  export function apply(ctx, config) {
137
166
  ctx.inject(['webServer', 'loader'], (hostCtx) => {
@@ -144,14 +173,18 @@ export function apply(ctx, config) {
144
173
  // its name, and never with somebody else's.
145
174
  const profileContext = ctx.get('profileContext');
146
175
  const launched = launchedProfile(profileContext);
147
- // The official Electron app owns this profile. Its CLI explicitly
148
- // refuses `--profile desktop`; use the app's pluginManager service.
176
+ // The official Electron app owns this profile. Its CLI refuses
177
+ // `--profile desktop` (outright up to dsh 0.2.0-rc.1; from rc.2 it
178
+ // admits only Desktop's own carrier, and only with Desktop fully quit —
179
+ // never from inside the running app this market lives in), so use the
180
+ // app's pluginManager service.
149
181
  // Looking it up at request time lets the market mount before the
150
182
  // service while still failing closed if it never becomes available.
151
183
  // The dsh CLI refuses a profile by NAME — `profile.toLowerCase() ===
152
- // "desktop"` (@deepseek-ai/dsh 0.1.7-alpha.2) — so a launched profile
153
- // with that name can never be changed through `dsh plugin`, whatever
154
- // the install layout. Detection follows the same rule rather than the
184
+ // "desktop"` (@deepseek-ai/dsh 0.1.7-alpha.2; from 0.2.0-rc.2 it lets
185
+ // only Desktop's own carrier through, with the app quit) — so from
186
+ // inside a running Desktop a launched profile with that name can never
187
+ // be changed through `dsh plugin`, whatever the install layout. Detection follows the same rule rather than the
155
188
  // app.asar anchor shape an earlier draft keyed on: the official desktop
156
189
  // host is not published, its layout could not be checked, and a host
157
190
  // that missed the anchor test fell straight back to the CLI and failed
@@ -162,7 +195,8 @@ export function apply(ctx, config) {
162
195
  const configuredDesktop = configured !== undefined && configured.toLowerCase() === 'desktop';
163
196
  const officialElectron = launched !== undefined && launched.name.toLowerCase() === 'desktop';
164
197
  // An explicitly configured `profile: desktop` takes this branch too. The
165
- // CLI refuses that profile by NAME, so routing it there — which is what
198
+ // CLI refuses that profile by NAME (see above — from inside the running
199
+ // app on every version), so routing it there — which is what
166
200
  // an operator's own workaround for a host that hides `profileContext`
167
201
  // does (#744) — makes every install fail for certain. The configuration
168
202
  // still wins over the launcher for the name; it just cannot win a branch
@@ -178,6 +212,11 @@ export function apply(ctx, config) {
178
212
  const resolved = {
179
213
  profile: profileName,
180
214
  ...(profileDirectory === undefined ? {} : { profileDirectory }),
215
+ origin: {
216
+ profileSource: configured !== undefined ? 'configured (cordis.yml), official desktop branch' : 'launcher profileContext, official desktop branch',
217
+ launcherAtStart: describeProfileContext(profileContext),
218
+ launcherNow: () => describeProfileContext(ctx.get('profileContext')),
219
+ },
181
220
  desktopHost: true,
182
221
  allowRestart: false,
183
222
  maxSnapshots: config?.maxSnapshots,
@@ -209,6 +248,14 @@ export function apply(ctx, config) {
209
248
  const resolved = {
210
249
  profile: config?.profile ?? launched?.name ?? argvProfile() ?? 'web',
211
250
  ...(useLaunchedDir ? { profileDirectory: launched.dir } : {}),
251
+ origin: {
252
+ profileSource: config?.profile !== undefined ? 'configured (cordis.yml)'
253
+ : launched !== undefined ? 'launcher profileContext'
254
+ : argvProfile() !== undefined ? '--profile flag'
255
+ : 'default (no configured profile, no launcher profileContext, no --profile flag)',
256
+ launcherAtStart: describeProfileContext(profileContext),
257
+ launcherNow: () => describeProfileContext(ctx.get('profileContext')),
258
+ },
212
259
  // Left UNDEFINED when unconfigured, deliberately: `?? true` here
213
260
  // would turn "the operator said nothing" into "the operator said
214
261
  // yes", and restartAllowed() could no longer tell them apart — which
@@ -244,6 +291,11 @@ export function apply(ctx, config) {
244
291
  const resolved = {
245
292
  profile: current.name,
246
293
  profileDirectory: current.dir,
294
+ origin: {
295
+ profileSource: 'desktop shell (desktopProfiles)',
296
+ launcherAtStart: describeProfileContext(ctx.get('profileContext')),
297
+ launcherNow: () => describeProfileContext(ctx.get('profileContext')),
298
+ },
247
299
  // The shell owns the window and the process lifecycle here; the
248
300
  // capability bits report that, and an explicit profile directory no
249
301
  // longer implies it (#639).
package/lib/install.js CHANGED
@@ -133,7 +133,25 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
133
133
  }
134
134
  let result = await run(profile, pluginArgs);
135
135
  const ok = (r) => r.exitCode === 0 && !r.timedOut && !r.cancelled;
136
- if (!ok(result) && !result.cancelled) {
136
+ // A shadowed rule written DURING the run is its own evidence (#732). The
137
+ // file was merged just before the run, so a duplicate now means pnpm — or
138
+ // the host driving it — appended one while this command ran, and every
139
+ // retry would trip on it. The repair below in the release-age branch only
140
+ // fired when pnpm's error text reached us; the official desktop host hands
141
+ // back its own message and `packageResult.output`, which need not carry
142
+ // `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`, so the line the host had just
143
+ // appended stayed dead (@Mlte0907, 1.66.7). The merge is a no-op when
144
+ // nothing is duplicated, so asking it costs one file read.
145
+ let repairedDuringRun = false;
146
+ if (!ok(result) && !result.cancelled && (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')) {
147
+ const mergedNow = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
148
+ if (mergedNow.length > 0) {
149
+ repairedDuringRun = true;
150
+ logEvent('warn', 'install', `a second minimumReleaseAgeExclude rule for ${mergedNow.join(', ')} was appended during the run, and pnpm honours only the first, shadowing it (#732) — merged them into one and retrying once`);
151
+ result = await run(profile, pluginArgs);
152
+ }
153
+ }
154
+ if (!ok(result) && !result.cancelled && !repairedDuringRun) {
137
155
  const failure = classifyPnpmFailure(`${result.stderr}\n${result.stdout}`, result.exitCode);
138
156
  if (failure?.code === 'unparseable-build-key') {
139
157
  // The profile's own allowBuilds block is what fails, so no retry of the
@@ -19,7 +19,16 @@ function detail(error) {
19
19
  return 'plugin manager did not explain the failure';
20
20
  return JSON.stringify(error);
21
21
  }
22
- /** Never fall back to `dsh plugin --profile desktop`: that CLI is forbidden. */
22
+ /**
23
+ * Never fall back to `dsh plugin --profile desktop`.
24
+ *
25
+ * Up to dsh 0.2.0-rc.1 the CLI refused that profile outright. From rc.2 it
26
+ * accepts it only when Desktop's own carrier invokes it, with Desktop fully
27
+ * quit — it takes a 120 s lock on the profile's package.json and says "fully
28
+ * quit it before running". The market only ever runs inside a live Desktop, so
29
+ * for the market the answer is the same on every version: the app's own
30
+ * plugin manager is the only door.
31
+ */
23
32
  export function createOfficialDesktopRuntime(managerLookup, profileName,
24
33
  // Unused by the manager bridge — it takes the profile by NAME — and optional
25
34
  // because an explicitly configured `profile: desktop` may be the only thing