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 +8 -0
- package/lib/http.js +33 -0
- package/lib/index.js +60 -8
- package/lib/install.js +19 -1
- package/lib/official-desktop.js +10 -1
- package/lib/routes.js +245 -184
- package/lib/types/http.d.ts +24 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/official-desktop.d.ts +10 -1
- package/lib/types/routes.d.ts +18 -0
- package/package.json +1 -1
- package/src/http.ts +51 -0
- package/src/index.ts +59 -8
- package/src/install.ts +19 -1
- package/src/official-desktop.ts +10 -1
- package/src/routes.ts +265 -179
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 () => {
|
|
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
|
|
148
|
-
//
|
|
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
|
|
153
|
-
//
|
|
154
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
package/lib/official-desktop.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|