@huaqiu/dsh-tool-plugin-update 0.6.1 → 0.6.2

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/src/routes.ts CHANGED
@@ -17,6 +17,21 @@
17
17
  * POST /plugin-update/ignore → POST /api/v1/plugin-updates/ignore {setVersion}
18
18
  * POST /plugin-update/unignore → POST /api/v1/plugin-updates/unignore {setVersion}
19
19
  * GET /plugin-update/runtime → GET /api/v1/runtime-update
20
+ * POST /plugin-update/runtime-request
21
+ * → POST /api/v1/runtime-update/request {targetVersion}
22
+ * GET /plugin-update/runtime-source
23
+ * → GET /api/v1/runtime-update/source
24
+ * POST /plugin-update/runtime-source
25
+ * → POST /api/v1/runtime-update/source {url}
26
+ * GET /plugin-update/plugin-source
27
+ * → GET /api/v1/plugin-updates/source
28
+ * POST /plugin-update/plugin-source
29
+ * → POST /api/v1/plugin-updates/source {url}
30
+ *
31
+ * The runtime-update request is user-confirmed in this plugin's review panel:
32
+ * hq-edge validates the version against a fresh manifest, downloads and
33
+ * verifies the bundle, stages it, then hands it to the EDA host and answers
34
+ * `202 Accepted` before shutting itself down to be swapped and relaunched.
20
35
  *
21
36
  * @module
22
37
  */
@@ -27,6 +42,23 @@ export const UPDATE_ROUTE_PREFIX = '/api/v1/huaqiu/plugin-update'
27
42
  /** End-to-end budget for one forwarded request, in milliseconds. */
28
43
  export const DEFAULT_FORWARD_TIMEOUT_MS = 30_000
29
44
 
45
+ /**
46
+ * Budget for `POST /runtime-update/request` — deliberately far larger than
47
+ * {@link DEFAULT_FORWARD_TIMEOUT_MS}.
48
+ *
49
+ * This route is not a quick RPC. Before it answers, hq-edge re-reads the
50
+ * update manifest, downloads the entire runtime bundle (hundreds of MB),
51
+ * verifies its SHA-256, extracts it into the shared staging root and validates
52
+ * the layout + version stamp. Measured end-to-end against a local update
53
+ * server: 56 s (16:49:49 → 16:50:45).
54
+ *
55
+ * Under the generic 30 s budget the forwarder aborts mid-flight and answers
56
+ * `504 hq-edge request timed out after 30000ms`, so the panel reports a failure
57
+ * for an update that is still running perfectly well — and which then goes on
58
+ * to complete, swap the runtime and restart HQ Edge underneath the error toast.
59
+ */
60
+ export const RUNTIME_REQUEST_TIMEOUT_MS = 600_000
61
+
30
62
  export type BaseUrlResolver = () => string | undefined
31
63
 
32
64
  function sendJson(res: ServerResponse, status: number, body: unknown): void {
@@ -66,21 +98,76 @@ function readBody(req: IncomingMessage): Promise<string> {
66
98
  })
67
99
  }
68
100
 
69
- /** Map a sub-path to { method, path } on the hq-edge API. */
70
- function routeFor(sub: string): { method: string; path: string } | null {
101
+ /**
102
+ * Map a sub-path + method to { method, path } on the hq-edge API.
103
+ *
104
+ * `timeoutMs` overrides {@link DEFAULT_FORWARD_TIMEOUT_MS} for routes whose
105
+ * upstream work legitimately outlasts it (only `/runtime-request` today).
106
+ *
107
+ * The method is part of the lookup because one sub-path can carry both a read
108
+ * and a write: `/runtime-source` is GET for the current value and POST to
109
+ * change it. A method the route does not declare is `null`, which the
110
+ * forwarder answers 405 rather than forwarding a GET to a POST-only upstream
111
+ * and surfacing hq-edge's 404 as a plugin bug.
112
+ */
113
+ function routeFor(
114
+ sub: string,
115
+ method: string,
116
+ ): { method: string; path: string; timeoutMs?: number } | null {
117
+ if (sub === '/runtime-source') {
118
+ if (method === 'GET') return { method: 'GET', path: '/api/v1/runtime-update/source' }
119
+ if (method === 'POST') return { method: 'POST', path: '/api/v1/runtime-update/source' }
120
+ return null
121
+ }
122
+ if (sub === '/plugin-source') {
123
+ if (method === 'GET') return { method: 'GET', path: '/api/v1/plugin-updates/source' }
124
+ if (method === 'POST') return { method: 'POST', path: '/api/v1/plugin-updates/source' }
125
+ return null
126
+ }
71
127
  if (sub === '/report' || sub === '') {
72
- return { method: 'GET', path: '/api/v1/plugin-updates' }
128
+ return method === 'GET' ? { method: 'GET', path: '/api/v1/plugin-updates' } : null
73
129
  }
74
130
  if (sub === '/runtime') {
75
- return { method: 'GET', path: '/api/v1/runtime-update' }
131
+ return method === 'GET' ? { method: 'GET', path: '/api/v1/runtime-update' } : null
132
+ }
133
+ if (sub === '/runtime-request') {
134
+ return method === 'POST'
135
+ ? {
136
+ method: 'POST',
137
+ path: '/api/v1/runtime-update/request',
138
+ timeoutMs: RUNTIME_REQUEST_TIMEOUT_MS,
139
+ }
140
+ : null
141
+ }
142
+ if (sub === '/check') {
143
+ return method === 'POST' ? { method: 'POST', path: '/api/v1/plugin-updates/check' } : null
144
+ }
145
+ if (sub === '/install') {
146
+ return method === 'POST' ? { method: 'POST', path: '/api/v1/plugin-updates/install' } : null
147
+ }
148
+ if (sub === '/ignore') {
149
+ return method === 'POST' ? { method: 'POST', path: '/api/v1/plugin-updates/ignore' } : null
150
+ }
151
+ if (sub === '/unignore') {
152
+ return method === 'POST' ? { method: 'POST', path: '/api/v1/plugin-updates/unignore' } : null
76
153
  }
77
- if (sub === '/check') return { method: 'POST', path: '/api/v1/plugin-updates/check' }
78
- if (sub === '/install') return { method: 'POST', path: '/api/v1/plugin-updates/install' }
79
- if (sub === '/ignore') return { method: 'POST', path: '/api/v1/plugin-updates/ignore' }
80
- if (sub === '/unignore') return { method: 'POST', path: '/api/v1/plugin-updates/unignore' }
81
154
  return null
82
155
  }
83
156
 
157
+ /** Every sub-path this forwarder serves — used to tell a 404 from a 405. */
158
+ const ROUTE_SUB_PATHS = new Set([
159
+ '',
160
+ '/report',
161
+ '/check',
162
+ '/install',
163
+ '/ignore',
164
+ '/unignore',
165
+ '/runtime',
166
+ '/runtime-request',
167
+ '/runtime-source',
168
+ '/plugin-source',
169
+ ])
170
+
84
171
  /** Create the prefix-route handler forwarding to hq-edge. */
85
172
  export function createUpdateForwarder(
86
173
  baseUrlResolver: BaseUrlResolver,
@@ -92,9 +179,19 @@ export function createUpdateForwarder(
92
179
  sendJson(res, 404, { error: 'not found' })
93
180
  return
94
181
  }
95
- const target = routeFor(sub)
182
+ // HEAD reads like GET: it never carried its own route, and the upstream
183
+ // has no HEAD semantics to preserve.
184
+ const method = req.method === 'HEAD' ? 'GET' : (req.method ?? '')
185
+ const target = routeFor(sub, method)
96
186
  if (target === null) {
97
- sendJson(res, 404, { error: 'not found' })
187
+ // Either an unknown sub-path (404) or a known one under a method it does
188
+ // not serve (405). Both are client errors caught here, not upstream.
189
+ sendJson(res, sub === '' || ROUTE_SUB_PATHS.has(sub) ? 405 : 404, {
190
+ error:
191
+ sub === '' || ROUTE_SUB_PATHS.has(sub)
192
+ ? `method ${req.method} not allowed on ${sub || '/'}`
193
+ : 'not found',
194
+ })
98
195
  return
99
196
  }
100
197
  if (req.method !== 'GET' && req.method !== 'POST' && req.method !== 'HEAD') {
@@ -115,8 +212,9 @@ export function createUpdateForwarder(
115
212
  return
116
213
  }
117
214
  }
215
+ const budget = target.timeoutMs ?? timeoutMs
118
216
  const controller = new AbortController()
119
- const timer = setTimeout(() => controller.abort(), timeoutMs)
217
+ const timer = setTimeout(() => controller.abort(), budget)
120
218
  try {
121
219
  const upstream = await fetch(`${base}${target.path}`, {
122
220
  method: target.method,
@@ -138,7 +236,7 @@ export function createUpdateForwarder(
138
236
  } catch (err) {
139
237
  const message = err instanceof Error ? err.message : String(err)
140
238
  if (err instanceof Error && err.name === 'AbortError') {
141
- sendJson(res, 504, { error: `hq-edge request timed out after ${timeoutMs}ms` })
239
+ sendJson(res, 504, { error: `hq-edge request timed out after ${budget}ms` })
142
240
  return
143
241
  }
144
242
  sendJson(res, 502, { error: `hq-edge request failed: ${message}` })