wldx-cli 0.1.1 → 1.1.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/README.md +281 -37
- package/package.json +9 -6
- package/src/chat/format.mjs +129 -0
- package/src/chat/lib.mjs +210 -0
- package/src/chat/watch.mjs +282 -0
- package/src/cli.mjs +13 -132
- package/src/commands/calls.mjs +470 -0
- package/src/commands/channel.mjs +404 -0
- package/src/commands/chat.mjs +444 -61
- package/src/commands/conversations.mjs +368 -0
- package/src/commands/file.mjs +80 -0
- package/src/commands/inbox.mjs +108 -0
- package/src/commands/index.mjs +33 -12
- package/src/commands/live.mjs +462 -0
- package/src/commands/meetings.mjs +386 -0
- package/src/commands/phone.mjs +564 -0
- package/src/commands/reports.mjs +606 -0
- package/src/commands/section.mjs +101 -0
- package/src/commands/user.mjs +159 -0
- package/src/history/text.mjs +86 -0
- package/src/history/wda.mjs +171 -0
- package/src/live/socket.mjs +139 -0
- package/src/live/stats.mjs +243 -0
- package/src/live/text.mjs +318 -0
- package/src/live/views.mjs +391 -0
- package/src/phone/control.mjs +437 -0
- package/src/phone/events.mjs +220 -0
- package/src/phone/pbx.mjs +158 -0
- package/src/phone/sip.mjs +386 -0
- package/src/phone/xml.mjs +57 -0
- package/src/stream.mjs +12 -14
- package/src/version.mjs +0 -1
- package/src/auth.mjs +0 -128
- package/src/commands/auth.mjs +0 -32
- package/src/completion.mjs +0 -105
- package/src/errors.mjs +0 -30
- package/src/http.mjs +0 -47
package/src/auth.mjs
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// The Wildix login of the user, shared with the wildix-auth agent skill: one file per account in
|
|
2
|
-
// $WILDIX_CONFIG_DIR/tokens (default ~/.wildix/tokens), named <e-mail with @ as _at_>.prod.json, holding the Cognito
|
|
3
|
-
// set {IdToken, AccessToken, RefreshToken, ExpiresIn, email, savedAt}. An expired IdToken is renewed with the
|
|
4
|
-
// RefreshToken and written back in the same format, so the skill and the CLI keep using one session.
|
|
5
|
-
//
|
|
6
|
-
// The token is never printed and never put on a command line or into the environment of a child process.
|
|
7
|
-
import crypto from 'node:crypto';
|
|
8
|
-
import fs from 'node:fs';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import {AuthError} from './errors.mjs';
|
|
12
|
-
import {USER_AGENT} from './version.mjs';
|
|
13
|
-
|
|
14
|
-
export const CLIENT_ID = '2un9vsk5ghu0ocurajqe4a6l1e';
|
|
15
|
-
export const COGNITO_URL = 'https://cognito-idp.eu-central-1.amazonaws.com/';
|
|
16
|
-
const RENEW_EARLY_MS = 60_000;
|
|
17
|
-
export const LOGIN_HINT = 'Log in with the wildix-auth agent skill (npx skills add gorets/wildix-skills -s wildix-auth); `wx login` is not there yet.';
|
|
18
|
-
|
|
19
|
-
export const configDir = (env = process.env) => env.WILDIX_CONFIG_DIR || path.join(os.homedir(), '.wildix');
|
|
20
|
-
export const tokensDir = (env = process.env) => path.join(configDir(env), 'tokens');
|
|
21
|
-
|
|
22
|
-
// The e-mail becomes part of a file name: letters, digits and . _ % + - @ only, no "..".
|
|
23
|
-
export function tokenFile(email, env = process.env) {
|
|
24
|
-
if (!/^[A-Za-z0-9._%+@-]+$/.test(email) || email.includes('..')) throw new AuthError(`Not a valid e-mail: ${email}`);
|
|
25
|
-
return path.join(tokensDir(env), `${email.replace(/@/g, '_at_')}.prod.json`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readJson(file) {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
|
-
} catch {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function jwtClaims(token) {
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString('utf8'));
|
|
39
|
-
} catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// When the IdToken of a saved set stops being valid, in ms: its `exp` claim, else savedAt + ExpiresIn, else 0.
|
|
45
|
-
export function expiresAt(set) {
|
|
46
|
-
const exp = jwtClaims(set?.IdToken).exp;
|
|
47
|
-
if (Number.isFinite(exp)) return exp * 1000;
|
|
48
|
-
const saved = Date.parse(set?.savedAt ?? '');
|
|
49
|
-
return Number.isFinite(saved) && Number.isFinite(set?.ExpiresIn) ? saved + set.ExpiresIn * 1000 : 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// The saved sessions: [{email, expiresAt}], never the tokens.
|
|
53
|
-
export function sessions(env = process.env) {
|
|
54
|
-
const dir = tokensDir(env);
|
|
55
|
-
let names;
|
|
56
|
-
try {
|
|
57
|
-
names = fs.readdirSync(dir).filter((n) => n.endsWith('.prod.json')).sort();
|
|
58
|
-
} catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
return names
|
|
62
|
-
.map((n) => readJson(path.join(dir, n)))
|
|
63
|
-
.filter((set) => typeof set?.email === 'string')
|
|
64
|
-
.map((set) => ({email: set.email, expiresAt: expiresAt(set)}));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Which account: --account, else WILDIX_EMAIL, else the only saved session.
|
|
68
|
-
export function pickAccount(env = process.env, account) {
|
|
69
|
-
const email = account || env.WILDIX_EMAIL;
|
|
70
|
-
if (email) return email;
|
|
71
|
-
const list = sessions(env);
|
|
72
|
-
if (!list.length) throw new AuthError(`Not logged in to Wildix. ${LOGIN_HINT}`);
|
|
73
|
-
if (list.length > 1) {
|
|
74
|
-
throw new AuthError(`Several Wildix accounts are logged in. Pick one with --account or WILDIX_EMAIL:\n${list.map((s) => ` ${s.email}`).join('\n')}`);
|
|
75
|
-
}
|
|
76
|
-
return list[0].email;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// The format of the skill: whole seconds, no milliseconds (its bash parses it with `date -f %Y-%m-%dT%H:%M:%SZ`).
|
|
80
|
-
export const stamp = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
81
|
-
|
|
82
|
-
// Writes a token set privately (0600) and atomically; a set without IdToken, RefreshToken and ExpiresIn is refused.
|
|
83
|
-
export function saveTokens(file, set) {
|
|
84
|
-
if (typeof set?.IdToken !== 'string' || typeof set?.RefreshToken !== 'string' || typeof set?.ExpiresIn !== 'number') {
|
|
85
|
-
throw new AuthError('The answer carried no complete set of tokens: nothing was saved.');
|
|
86
|
-
}
|
|
87
|
-
const dir = path.dirname(file);
|
|
88
|
-
fs.mkdirSync(dir, {recursive: true, mode: 0o700});
|
|
89
|
-
const tmp = path.join(dir, `.${path.basename(file)}.${crypto.randomBytes(6).toString('hex')}`);
|
|
90
|
-
fs.writeFileSync(tmp, `${JSON.stringify(set, null, 2)}\n`, {mode: 0o600, flag: 'wx'});
|
|
91
|
-
fs.renameSync(tmp, file);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function renew(email, set, {env, fetchImpl, now}) {
|
|
95
|
-
const res = await fetchImpl(COGNITO_URL, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: {
|
|
98
|
-
'user-agent': USER_AGENT,
|
|
99
|
-
'content-type': 'application/x-amz-json-1.1',
|
|
100
|
-
'cache-control': 'no-store',
|
|
101
|
-
'x-amz-target': 'AWSCognitoIdentityProviderService.InitiateAuth',
|
|
102
|
-
},
|
|
103
|
-
body: JSON.stringify({AuthFlow: 'REFRESH_TOKEN_AUTH', ClientId: CLIENT_ID, AuthParameters: {REFRESH_TOKEN: set.RefreshToken}, ClientMetadata: {}}),
|
|
104
|
-
redirect: 'error',
|
|
105
|
-
signal: AbortSignal.timeout(30_000),
|
|
106
|
-
});
|
|
107
|
-
const body = await res.json().catch(() => ({}));
|
|
108
|
-
const result = body?.AuthenticationResult;
|
|
109
|
-
if (!res.ok || body?.__type || typeof result?.IdToken !== 'string') {
|
|
110
|
-
throw new AuthError(`The session of ${email} could not be renewed (${body?.message || body?.__type || `HTTP ${res.status}`}). ${LOGIN_HINT}`);
|
|
111
|
-
}
|
|
112
|
-
// Cognito does not send a new RefreshToken on a refresh: the saved one is kept.
|
|
113
|
-
const next = {...result, email, savedAt: stamp(now), RefreshToken: set.RefreshToken, ExpiresIn: result.ExpiresIn ?? set.ExpiresIn};
|
|
114
|
-
saveTokens(tokenFile(email, env), next);
|
|
115
|
-
return next;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// A valid IdToken of the account: WILDIX_ID_TOKEN as given (tests, CI), else the saved session, renewed when it
|
|
119
|
-
// expires within a minute.
|
|
120
|
-
export async function getToken({account, env = process.env, fetchImpl = globalThis.fetch, now = Date.now()} = {}) {
|
|
121
|
-
if (env.WILDIX_ID_TOKEN) return env.WILDIX_ID_TOKEN;
|
|
122
|
-
const email = pickAccount(env, account);
|
|
123
|
-
const set = readJson(tokenFile(email, env));
|
|
124
|
-
if (typeof set?.IdToken !== 'string') throw new AuthError(`No saved session for ${email}. ${LOGIN_HINT}`);
|
|
125
|
-
if (expiresAt(set) - RENEW_EARLY_MS > now) return set.IdToken;
|
|
126
|
-
if (typeof set.RefreshToken !== 'string') throw new AuthError(`The session of ${email} has expired. ${LOGIN_HINT}`);
|
|
127
|
-
return (await renew(email, set, {env, fetchImpl, now})).IdToken;
|
|
128
|
-
}
|
package/src/commands/auth.mjs
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// wx auth: the saved Wildix sessions (shared with the wildix-auth agent skill). Never prints a token.
|
|
2
|
-
import {AuthError} from '../errors.mjs';
|
|
3
|
-
import {pickAccount, sessions} from '../auth.mjs';
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
summary: 'the Wildix accounts you are logged in with',
|
|
7
|
-
commands: {
|
|
8
|
-
status: {
|
|
9
|
-
summary: 'saved sessions, which one is used and until when its token is valid (no network)',
|
|
10
|
-
usage: 'wx auth status',
|
|
11
|
-
options: {},
|
|
12
|
-
args: [0, 0],
|
|
13
|
-
async run({values, env, now}) {
|
|
14
|
-
let account = null;
|
|
15
|
-
try {
|
|
16
|
-
account = pickAccount(env, values.account);
|
|
17
|
-
} catch (e) {
|
|
18
|
-
if (!(e instanceof AuthError)) throw e;
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
account,
|
|
22
|
-
tokenFromEnvironment: Boolean(env.WILDIX_ID_TOKEN),
|
|
23
|
-
sessions: sessions(env).map((s) => ({
|
|
24
|
-
email: s.email,
|
|
25
|
-
tokenValidUntil: s.expiresAt ? new Date(s.expiresAt).toISOString() : null,
|
|
26
|
-
expired: s.expiresAt <= now,
|
|
27
|
-
})),
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
};
|
package/src/completion.mjs
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// Shell completion. `wx completion <shell>` prints a small script; on every Tab it runs
|
|
2
|
-
// `wx __complete -- <words after wx, the last one being the word under the cursor>`, which prints the candidates,
|
|
3
|
-
// one per line as "value<TAB>description". The candidates come from the command definitions (and the saved
|
|
4
|
-
// accounts for --account), so completion follows the commands without a regenerated script. No network.
|
|
5
|
-
import {sessions} from './auth.mjs';
|
|
6
|
-
import {COMMON_OPTIONS, GROUPS, commandOf, groupOf} from './commands/index.mjs';
|
|
7
|
-
|
|
8
|
-
export const SHELLS = ['bash', 'zsh', 'fish'];
|
|
9
|
-
|
|
10
|
-
export const TOP_LEVEL = {
|
|
11
|
-
completion: 'print the shell completion script (bash, zsh, fish)',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const TOP_OPTIONS = {'--help': 'help of wx', '--version': 'the version of wx'};
|
|
15
|
-
|
|
16
|
-
function valuesOf(option, env) {
|
|
17
|
-
if (option.choices) return option.choices.map((v) => [v]);
|
|
18
|
-
if (option.complete === 'accounts') return sessions(env).map((s) => [s.email]);
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// The candidates for the word under the cursor: ["value\tdescription", ...].
|
|
23
|
-
export function complete(words, env = process.env) {
|
|
24
|
-
const cur = words.length ? words[words.length - 1] : '';
|
|
25
|
-
const done = words.slice(0, -1);
|
|
26
|
-
const offer = (pairs) => pairs.filter(([v]) => v.startsWith(cur)).map(([v, d]) => (d ? `${v}\t${d}` : v));
|
|
27
|
-
|
|
28
|
-
if (done.length === 0) {
|
|
29
|
-
return offer([
|
|
30
|
-
...Object.entries(GROUPS).map(([name, g]) => [name, g.summary]),
|
|
31
|
-
...Object.entries(TOP_LEVEL),
|
|
32
|
-
...(cur.startsWith('-') ? Object.entries(TOP_OPTIONS) : []),
|
|
33
|
-
]);
|
|
34
|
-
}
|
|
35
|
-
if (done[0] === 'completion') return done.length === 1 ? offer(SHELLS.map((s) => [s, `the script for ${s}`])) : [];
|
|
36
|
-
|
|
37
|
-
const group = groupOf(done[0]);
|
|
38
|
-
if (!group) return [];
|
|
39
|
-
if (done.length === 1) return offer(Object.entries(group.commands).map(([name, cmd]) => [name, cmd.summary]));
|
|
40
|
-
|
|
41
|
-
const cmd = commandOf(group, done[1]);
|
|
42
|
-
if (!cmd) return [];
|
|
43
|
-
const options = {...cmd.options, ...COMMON_OPTIONS};
|
|
44
|
-
const prev = done[done.length - 1];
|
|
45
|
-
if (done.length > 2 && prev.startsWith('--')) {
|
|
46
|
-
const option = Object.hasOwn(options, prev.slice(2)) ? options[prev.slice(2)] : undefined;
|
|
47
|
-
if (option?.type === 'string') return offer(valuesOf(option, env));
|
|
48
|
-
}
|
|
49
|
-
if (cur.startsWith('-')) {
|
|
50
|
-
const used = new Set(done.slice(2).filter((w) => w.startsWith('--')).map((w) => w.slice(2)));
|
|
51
|
-
return offer(Object.entries(options).filter(([name, o]) => o.multiple || !used.has(name)).map(([name, o]) => [`--${name}`, o.description]));
|
|
52
|
-
}
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const SCRIPTS = {
|
|
57
|
-
bash: `# wx completion for bash. Add to ~/.bashrc: eval "$(wx completion bash)"
|
|
58
|
-
_wx_complete() {
|
|
59
|
-
# The words are taken before IFS changes: bash 3.2 (macOS) joins the elements of a quoted array slice when IFS
|
|
60
|
-
# is a newline.
|
|
61
|
-
local words=("\${COMP_WORDS[@]:1:COMP_CWORD}") out line
|
|
62
|
-
COMPREPLY=()
|
|
63
|
-
out=$(wx __complete -- "\${words[@]}" 2>/dev/null) || return 0
|
|
64
|
-
local IFS=$'\\n'
|
|
65
|
-
for line in $out; do
|
|
66
|
-
COMPREPLY+=("\${line%%$'\\t'*}")
|
|
67
|
-
done
|
|
68
|
-
}
|
|
69
|
-
complete -F _wx_complete wx
|
|
70
|
-
`,
|
|
71
|
-
zsh: `# wx completion for zsh. Add to ~/.zshrc (after compinit): eval "$(wx completion zsh)"
|
|
72
|
-
_wx() {
|
|
73
|
-
local -a lines items
|
|
74
|
-
local line
|
|
75
|
-
lines=("\${(@f)$(wx __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)}")
|
|
76
|
-
for line in $lines; do
|
|
77
|
-
[[ -n $line ]] || continue
|
|
78
|
-
if [[ $line == *$'\\t'* ]]; then items+=("\${\${line%%$'\\t'*}//:/\\\\:}:\${line#*$'\\t'}")
|
|
79
|
-
else items+=("\${line//:/\\\\:}"); fi
|
|
80
|
-
done
|
|
81
|
-
(( \${#items} )) && _describe -t wx-candidates 'wx' items
|
|
82
|
-
}
|
|
83
|
-
(( \${+functions[compdef]} )) || { autoload -Uz compinit && compinit; }
|
|
84
|
-
compdef _wx wx
|
|
85
|
-
`,
|
|
86
|
-
fish: `# wx completion for fish. Save as ~/.config/fish/completions/wx.fish: wx completion fish > ~/.config/fish/completions/wx.fish
|
|
87
|
-
complete -c wx -f -a '(wx __complete -- (commandline -opc)[2..-1] (commandline -ct) 2>/dev/null)'
|
|
88
|
-
`,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export function completionScript(shell) {
|
|
92
|
-
return Object.hasOwn(SCRIPTS, shell) ? SCRIPTS[shell] : undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function completionUsage() {
|
|
96
|
-
return [
|
|
97
|
-
'Usage: wx completion [bash|zsh|fish] (default: the shell of $SHELL)',
|
|
98
|
-
'',
|
|
99
|
-
'Prints the completion script. To turn completion on for good:',
|
|
100
|
-
' bash: echo \'eval "$(wx completion bash)"\' >> ~/.bashrc',
|
|
101
|
-
' zsh: echo \'eval "$(wx completion zsh)"\' >> ~/.zshrc',
|
|
102
|
-
' fish: wx completion fish > ~/.config/fish/completions/wx.fish',
|
|
103
|
-
'then open a new terminal. It completes an installed `wx` (npm i -g wldx-cli), not `npx wldx-cli`.',
|
|
104
|
-
].join('\n');
|
|
105
|
-
}
|
package/src/errors.mjs
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Errors that map to exit codes: UsageError 64 (EX_USAGE), AuthError 2, anything else 1.
|
|
2
|
-
|
|
3
|
-
export class UsageError extends Error {
|
|
4
|
-
constructor(message, usage) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = 'UsageError';
|
|
7
|
-
this.usage = usage;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class AuthError extends Error {
|
|
12
|
-
constructor(message) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = 'AuthError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const NOT_IMPLEMENTED = /is not implemented/;
|
|
19
|
-
|
|
20
|
-
export class HttpError extends Error {
|
|
21
|
-
constructor(status, method, path, body, service = 'the service') {
|
|
22
|
-
const lines = [`HTTP ${status} from ${service} (${method} ${path})`];
|
|
23
|
-
if (body) lines.push(body.length > 2000 ? `${body.slice(0, 2000)}…` : body);
|
|
24
|
-
if (status === 401) lines.push('The token was rejected: log in again.');
|
|
25
|
-
if (NOT_IMPLEMENTED.test(body ?? '')) lines.push('The service does not implement this operation yet. Do not retry.');
|
|
26
|
-
super(lines.join('\n'));
|
|
27
|
-
this.name = 'HttpError';
|
|
28
|
-
this.status = status;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/http.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {HttpError} from './errors.mjs';
|
|
2
|
-
import {USER_AGENT} from './version.mjs';
|
|
3
|
-
|
|
4
|
-
const SEGMENT = /^[A-Za-z0-9_+][A-Za-z0-9_.:+-]*$/;
|
|
5
|
-
|
|
6
|
-
// A tagged template for request paths: every interpolated value is one path segment of id characters (no "/",
|
|
7
|
-
// "?", "#", "..", nothing empty), so an id taken from fetched data cannot rewrite the path.
|
|
8
|
-
// apiPath`/channels/${id}/messages`
|
|
9
|
-
export function apiPath(strings, ...values) {
|
|
10
|
-
return strings.reduce((out, s, i) => {
|
|
11
|
-
if (i === 0) return s;
|
|
12
|
-
const v = String(values[i - 1] ?? '');
|
|
13
|
-
if (!SEGMENT.test(v) || v.includes('..')) throw new Error(`An id is not safe in a request path: ${JSON.stringify(v)}`);
|
|
14
|
-
return out + encodeURIComponent(v) + s;
|
|
15
|
-
}, '');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// "?a=1&b=2" from an object; undefined, null, false and '' are left out.
|
|
19
|
-
export function query(params) {
|
|
20
|
-
const q = new URLSearchParams();
|
|
21
|
-
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== false && v !== '') q.append(k, String(v));
|
|
22
|
-
const s = q.toString();
|
|
23
|
-
return s ? `?${s}` : '';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// One JSON request. Redirects are refused, so the token never follows one to another host. Returns the parsed
|
|
27
|
-
// answer, or {ok: true} when it is empty; throws HttpError on a status other than 2xx.
|
|
28
|
-
export async function request(url, {method = 'GET', token, body, service, fetchImpl = globalThis.fetch, timeoutMs = 60_000} = {}) {
|
|
29
|
-
const headers = {'user-agent': USER_AGENT, accept: 'application/json'};
|
|
30
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
31
|
-
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
32
|
-
const res = await fetchImpl(url, {
|
|
33
|
-
method,
|
|
34
|
-
headers,
|
|
35
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
36
|
-
redirect: 'error',
|
|
37
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
38
|
-
});
|
|
39
|
-
const text = await res.text();
|
|
40
|
-
if (!res.ok) throw new HttpError(res.status, method, new URL(url).pathname, text, service);
|
|
41
|
-
if (!text) return {ok: true};
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(text);
|
|
44
|
-
} catch {
|
|
45
|
-
return {text};
|
|
46
|
-
}
|
|
47
|
-
}
|