|
6 | 6 | // unsuffixed `DstackClient` names since 0.6.0. |
7 | 7 |
|
8 | 8 | import { send_rpc_request } from './send-rpc-request' |
9 | | -import { to_hex, throwOnRpcError, resolveDstackEndpoint } from './shared' |
10 | | - |
11 | | -/** An even number of hex digits, and nothing else. */ |
12 | | -const HEX_ONLY = /^(?:[0-9a-fA-F]{2})*$/ |
13 | | - |
14 | | -/** |
15 | | - * Decode a wire hex string, or say which field was malformed. |
16 | | - * |
17 | | - * Strict on purpose. Node's hex decoder stops at the first pair it cannot |
18 | | - * parse and returns the prefix it managed, without error: `Buffer.from( |
19 | | - * '0102zz', 'hex')` is two bytes, and an odd-length string loses its last |
20 | | - * digit. These fields are private keys, signature chain links and application |
21 | | - * identity -- handing back a silently truncated one is worse than throwing, |
22 | | - * and Rust, Python and Go all refuse the same input. |
23 | | - */ |
24 | | -function decode_hex(value: unknown, field: string): Uint8Array { |
25 | | - // The type check is not redundant with the regex, and dropping it is a |
26 | | - // silent-wrong-value bug rather than a style regression. `RegExp.test` |
27 | | - // stringifies its argument, so a one-element array passes -- `['00112233']` |
28 | | - // becomes `'00112233'` -- and `Buffer.from` then ignores the `'hex'` |
29 | | - // argument for a non-string input and coerces the elements as octets: |
30 | | - // `Number('00112233') & 0xff`, one attacker-chosen byte, no error. TypeScript |
31 | | - // cannot stop this because a JSON response is `any` at runtime. |
32 | | - if (typeof value !== 'string') { |
33 | | - throw new Error( |
34 | | - `the agent returned a malformed ${field}: expected a hex string, got ${ |
35 | | - value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value}` |
36 | | - ) |
37 | | - } |
38 | | - if (!HEX_ONLY.test(value)) { |
39 | | - throw new Error( |
40 | | - `the agent returned a malformed ${field}: expected an even-length hex string` |
41 | | - ) |
42 | | - } |
43 | | - return new Uint8Array(Buffer.from(value, 'hex')) |
44 | | -} |
45 | | - |
46 | | -/** |
47 | | - * Decode a `bytes` field the proto declares required. |
48 | | - * |
49 | | - * Absence is an error rather than the empty default: `app_id` and `key` are |
50 | | - * answers the agent always has, so a response without one is a response that |
51 | | - * did not come from a working agent. An empty *string* still decodes to zero |
52 | | - * bytes, which is what every other SDK does with it. |
53 | | - */ |
54 | | -function from_hex(value: unknown, field: string): Uint8Array { |
55 | | - if (value === undefined) { |
56 | | - throw new Error(`the agent returned no ${field}`) |
57 | | - } |
58 | | - return decode_hex(value, field) |
59 | | -} |
| 9 | +import { |
| 10 | + to_hex, throwOnRpcError, resolveDstackEndpoint, |
| 11 | + decode_hex, from_hex, require_string, to_list, |
| 12 | +} from './shared' |
60 | 13 |
|
61 | 14 | /** |
62 | 15 | * Decode a `bytes` field, treating an absent key as the empty default. |
@@ -277,50 +230,6 @@ function to_string(value: unknown, field: string): string { |
277 | 230 | return require_string(value, field) |
278 | 231 | } |
279 | 232 |
|
280 | | -/** |
281 | | - * Read a `string` field the response is meaningless without. |
282 | | - * |
283 | | - * A bundle's `vendor` and `format` are what a caller dispatches on to pick a |
284 | | - * verifier, so handing back `undefined` there does not degrade the answer, it |
285 | | - * routes the evidence to no verifier at all -- quietly, since `undefined` |
286 | | - * matches no `case`. Rust and Python both make these required. |
287 | | - */ |
288 | | -function require_string(value: unknown, field: string): string { |
289 | | - if (typeof value !== 'string') { |
290 | | - throw new Error( |
291 | | - `the agent returned a malformed ${field}: expected a string, got ${ |
292 | | - value === undefined ? 'nothing' |
293 | | - : value === null ? 'null' |
294 | | - : Array.isArray(value) ? 'an array' : typeof value}` |
295 | | - ) |
296 | | - } |
297 | | - return value |
298 | | -} |
299 | | - |
300 | | -/** |
301 | | - * Read a `repeated` field, or say which one was not a list. |
302 | | - * |
303 | | - * `Array.isArray` rather than a truthiness check: a bare `.map()` on a `null` |
304 | | - * or absent field throws `TypeError: Cannot read properties of null`, which |
305 | | - * names no field and reads like an SDK bug rather than a bad response. |
306 | | - * |
307 | | - * `whenAbsent` follows the proto. A missing `boottime_gpu_evidence` is the |
308 | | - * empty list, because the field is only populated when asked for; a missing |
309 | | - * `bundles` or `signature_chain` is a malformed response, because those are |
310 | | - * the whole answer of the call that returns them. |
311 | | - */ |
312 | | -function to_list( |
313 | | - value: unknown, field: string, whenAbsent: 'empty' | 'error', |
314 | | -): unknown[] { |
315 | | - if (value === undefined && whenAbsent === 'empty') { |
316 | | - return [] |
317 | | - } |
318 | | - if (!Array.isArray(value)) { |
319 | | - throw new Error(`the agent returned a malformed ${field}: expected a list`) |
320 | | - } |
321 | | - return value |
322 | | -} |
323 | | - |
324 | 233 | /** |
325 | 234 | * Decode the bundles a v1 RPC returned. |
326 | 235 | * |
|
0 commit comments