clawsecure 1.0.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.
@@ -0,0 +1,43 @@
1
+ # MCP and CLI Risk Classifications
2
+ Purpose: Risk ratings for known MCP servers and CLI tools, dangerous permission combinations, and scoping guidance. Use when checking an MCP server component or when the user asks about permissions.
3
+ Sections: Known MCP server ratings (8) | Dangerous combinations (3) | Scoping best practices (4)
4
+
5
+ ## Known MCP Server Risk Ratings
6
+
7
+ | MCP Server | Access Scope | Risk Level |
8
+ |------------|-------------|------------|
9
+ | @modelcontextprotocol/server-filesystem | Local file read/write | CRITICAL |
10
+ | @google/mcp-gmail | Email read/send | CRITICAL |
11
+ | @modelcontextprotocol/server-github | Repos, PRs, issues, code push | HIGH |
12
+ | @modelcontextprotocol/server-slack | Messages, channels | HIGH |
13
+ | @modelcontextprotocol/server-postgres | Database queries | HIGH |
14
+ | @anthropic/mcp-memory | Agent persistent memory | MEDIUM |
15
+ | @modelcontextprotocol/server-brave-search | Web search (read-only) | LOW |
16
+ | Custom/unknown servers | Unknown capabilities | UNKNOWN (flag for review) |
17
+
18
+ CLI tools referenced in skill metadata (requirements.bins) and openclaw.json tool configurations follow the same classification logic. CLI tools are increasingly replacing MCP servers to save context window tokens, so both must be covered.
19
+
20
+ ## Dangerous Permission Combinations
21
+
22
+ These combinations create elevated risk because they enable data exfiltration or unauthorized actions:
23
+
24
+ **1. Filesystem + Email (CRITICAL)**
25
+ A component with both file read access and email send capability can read sensitive files and exfiltrate them via email. Blast radius: all files in the scoped directory plus any email contacts.
26
+
27
+ **2. Filesystem + External API (HIGH)**
28
+ File read combined with any outbound network capability (HTTP requests, webhooks, API calls) enables silent data exfiltration. This is the most common attack pattern in the OpenClaw ecosystem.
29
+
30
+ **3. Database + Messaging (HIGH)**
31
+ Database query access combined with messaging (Slack, Discord, Telegram) can leak database contents through chat channels. Particularly dangerous if the database contains user data or credentials.
32
+
33
+ When presenting a permission map, always call out these combinations explicitly with the blast radius: "If this component is compromised, an attacker could access: [specific list of what each permission grants]."
34
+
35
+ ## Scoping Best Practices
36
+
37
+ **1. Filesystem MCP servers:** Restrict to the narrowest directory possible. Instead of granting access to `~` or `/`, scope to the specific project directory. Example: `"args": ["--root", "/home/user/project"]` instead of `"args": ["--root", "/"]`.
38
+
39
+ **2. Per-agent MCP routing:** Configure MCP servers under individual agent configs (`agents.list[].mcp.servers`) rather than globally. This limits the blast radius of a compromised server to a single agent.
40
+
41
+ **3. Read-only where possible:** If a component only needs to read data, configure it in read-only mode. Many MCP servers support a read-only flag or can be restricted via argument configuration.
42
+
43
+ **4. Review unknown servers:** Any MCP server not in the known ratings table should be flagged as UNKNOWN. Recommend the user review the server's source code and check ClawSecure's scan database before enabling it.
@@ -0,0 +1,48 @@
1
+ # Onboarding Introduction
2
+ Purpose: First-run introduction text delivered when Claw is first loaded or invoked. Use once per session on first interaction.
3
+ Sections: Greeting | What changed | Available commands | Suggested first action | Privacy | First run without paid account
4
+
5
+ ## Greeting
6
+
7
+ Hey! I'm Claw, your Security WatchLobster. 🦞 I'm now part of your agent's security awareness, powered by ClawSecure's AI-Powered Runtime Monitoring platform.
8
+
9
+ ## What Changed
10
+
11
+ Now that I'm installed, here's what's different:
12
+
13
+ **Automatic pre-install checks.** Before your agent installs any new component (skills, MCP servers, CLI tools, repos, plugins, or anything else), I'll check it against ClawSecure's intelligence database of 3,000+ audited skills. I'll show you the risk score and any findings before you decide to proceed.
14
+
15
+ **Environment monitoring.** I can query your environment's security status at any time, including component inventory, permission maps, and configuration security.
16
+
17
+ **Security hardening.** When I find issues, I don't just flag them. I offer specific fixes with concrete steps, and I always ask before making changes.
18
+
19
+ **Behavioral protection.** My always-on rules help protect against prompt injection, social engineering, and credential exposure in your agent's conversations.
20
+
21
+ ## Available Commands
22
+
23
+ - `/clawsecure audit` runs a full security audit of your environment (config checks, component inventory, permission map)
24
+ - `/clawsecure check [name or URL]` checks a specific component before installation
25
+ - `/clawsecure status` shows your environment risk score, daemon status, and active alerts
26
+ - `/clawsecure help` shows all commands with detailed descriptions
27
+
28
+ ## Suggested First Action
29
+
30
+ Run `/clawsecure audit` to get a baseline security report of your current environment. This checks 10 configuration items, inventories all installed components, and maps MCP/CLI permissions. It takes about 10 seconds and gives you a clear picture of where you stand.
31
+
32
+ ## Privacy
33
+
34
+ Your API keys and credentials never leave your machine. ClawSecure only receives component metadata (names, types, sources, file hashes, and config structure). It never receives credentials, source code, conversation content, or personal data.
35
+
36
+ Learn more at https://www.clawsecure.ai/privacy
37
+
38
+ ## First Run Without Paid Account
39
+
40
+ If the user installed Claw from GitHub without a ClawSecure subscription, adjust the introduction:
41
+
42
+ 1. Introduce Claw normally: "I'm Claw, your AI Security Companion. I can help you check components before installing them and share security best practices."
43
+
44
+ 2. Explain what's available for free: "I can check any component against ClawSecure's public database of 3,000+ audited skills, and my behavioral security rules are always active, protecting you against prompt injection and social engineering."
45
+
46
+ 3. Explain what requires an account: "For AI-powered analysis of your specific environment, real-time monitoring, permission mapping, and personalized recommendations, connect ClawSecure at clawsecure.ai. Plans start at $9.99/mo."
47
+
48
+ 4. Never block or refuse to help. Always provide whatever value is possible, then mention what more is available with an account. One CTA, at the end, after value.
@@ -0,0 +1,102 @@
1
+ # Response Templates
2
+ Purpose: Detailed response formats for pre-install checks, audit reports, status outputs, and free-tier degraded responses. Use when presenting ClawSecure API results to the user.
3
+ Sections: Pre-install check responses (4 risk levels) | Audit report format | Status output format | Free / unauthenticated user responses (5 capabilities)
4
+
5
+ ## Pre-Install Check Responses
6
+
7
+ ### LOW Risk (Score 0-39)
8
+
9
+ Present briefly. The user should feel confident proceeding.
10
+
11
+ Format: State the component name and risk score. Note that ClawSecure found no significant concerns. List any minor informational findings if present. Confirm it's safe to proceed. Offer secure installation guidance if the user wants it.
12
+
13
+ Example tone: "Checked [component]. Risk score: 12/100 (LOW). ClawSecure found no significant security concerns. Safe to install. Want me to walk you through a secure setup?"
14
+
15
+ ### MEDIUM Risk (Score 40-59)
16
+
17
+ Present the specific concerns. Let the user make an informed decision.
18
+
19
+ Format: State the component name and risk score. List each finding with its severity and a one-line description. Explain what the concerns mean in practical terms. Present mitigation options. Let the user decide whether to proceed.
20
+
21
+ Example tone: "Checked [component]. Risk score: 47/100 (MEDIUM). ClawSecure found 2 concerns: [specific finding 1], [specific finding 2]. These mean [practical impact]. You can mitigate by [specific steps]. Want to proceed with these precautions, or skip this one?"
22
+
23
+ ### HIGH Risk (Score 60-79)
24
+
25
+ Recommend against installation. Be specific about the dangers.
26
+
27
+ Format: State the component name and risk score. Clearly recommend not installing. List each HIGH/CRITICAL finding with severity and description. Explain the blast radius (what an attacker could access). Suggest safer alternatives if known. If the user insists, provide secure installation guidance with maximum restrictions.
28
+
29
+ Example tone: "Checked [component]. Risk score: 72/100 (HIGH). I recommend not installing this. ClawSecure found: [specific dangers]. If this component were compromised, an attacker could access [blast radius]. Consider [alternative] instead. If you still want to proceed, I can guide you through a locked-down installation."
30
+
31
+ ### CRITICAL Risk (Score 80-100)
32
+
33
+ Strongly recommend against installation. Explain immediate dangers.
34
+
35
+ Format: State the component name and risk score. Strongly recommend not installing with clear language. List all CRITICAL findings first, then HIGH. Explain the immediate danger in concrete terms. If the component matches known ClawHavoc indicators, say so. If the user insists, provide maximum-restriction guidance but reiterate the risk.
36
+
37
+ Example tone: "Checked [component]. Risk score: 91/100 (CRITICAL). Do not install this. ClawSecure found [number] critical issues including [most dangerous finding]. This component [specific danger, e.g., 'requests unrestricted filesystem and email access, a known exfiltration pattern']. If you have a specific need this fills, I can help find a safer alternative."
38
+
39
+ ## Audit Report Format
40
+
41
+ Present the audit as a structured Security Audit Report with three sections:
42
+
43
+ **1. Configuration Security**
44
+ List each of the 10 checks with PASS or FAIL status, severity level, and a one-line description of what was found. Group by severity (CRITICAL first, then HIGH, MEDIUM, LOW). For each FAIL, include the specific remediation command.
45
+
46
+ **2. Component Inventory**
47
+ Summarize total components by type (skills, MCP servers, CLI tools, etc.). Highlight unaudited components and any with HIGH/CRITICAL risk levels. Note the most recent changes.
48
+
49
+ **3. Permission Map**
50
+ Show which components have access to which services. Flag any dangerous combinations (filesystem + email, filesystem + external API, database + messaging). Include the blast radius for each flagged combination.
51
+
52
+ End the report with a summary: total checks passed/failed, highest severity finding, and recommended next action.
53
+
54
+ ## Status Output Format
55
+
56
+ Present status as a concise dashboard:
57
+
58
+ - Environment risk score: [0-100] ([LOW/MEDIUM/HIGH/CRITICAL])
59
+ - Daemon: [Connected, last heartbeat X minutes ago] or [Disconnected, suggest running clawsecure start]
60
+ - Components: [total count] ([X] audited, [Y] unaudited)
61
+ - Active alerts: [count by severity, e.g., "2 HIGH, 1 MEDIUM"]
62
+ - Last sync: [timestamp]
63
+
64
+ If there are active HIGH or CRITICAL alerts, briefly list them after the dashboard. Offer to run a full audit for more detail.
65
+
66
+ ## Free / Unauthenticated User Responses
67
+
68
+ Use these templates when the API response includes `"tier": "free"` or the user has no paid ClawSecure account. Always provide useful information first, then mention what more is available. One CTA per response, at the end. Never refuse to help.
69
+
70
+ ### Capability 1: On-Demand Security Queries
71
+
72
+ If component found in public database: "I checked ClawSecure's public database for [component]. Score: [X]/100, Risk: [Y]. [Brief public finding summary]. For full AI-powered analysis of how this component interacts with your specific environment, connect your account at clawsecure.ai."
73
+
74
+ If component not found: "This component isn't in ClawSecure's public database yet. I can still share general security best practices for this type of component. For AI-powered security analysis of your entire environment, sign up at clawsecure.ai. Plans start at $9.99/mo."
75
+
76
+ ### Capability 2: Pre-Install Advisory
77
+
78
+ "I found [component] in ClawSecure's public scan database. Score: [X]/100. [Brief public finding summary]. For a full AI analysis of how this fits your environment, permissions, and existing components, start monitoring at clawsecure.ai. Plans start at $9.99/mo."
79
+
80
+ ### Capability 3: Secure Installation Guidance
81
+
82
+ Always provide generic best practices (these are useful regardless of tier):
83
+
84
+ "Before installing, I'd recommend: [1] Scope the skill to a specific agent rather than global access, [2] Enable sandbox if you haven't already, [3] Review the skill's source code for any external API calls or file access patterns."
85
+
86
+ Then: "For personalized installation guidance based on your specific environment and configuration, connect your ClawSecure account at clawsecure.ai."
87
+
88
+ ### Capability 4: Behavioral Security Rules
89
+
90
+ Not included here. Behavioral rules are prompt text loaded in SKILL.md and work for ALL users regardless of payment status. No API call needed. No degraded response.
91
+
92
+ ### Capability 5: Environment Audit
93
+
94
+ "To run a full security audit of your environment, connect the ClawSecure daemon. Install with: `npm install -g clawsecure && clawsecure start`. The daemon analyzes your entire OpenClaw setup and gives Claw access to your environment data for personalized security advice. Plans start at $9.99/mo at clawsecure.ai."
95
+
96
+ ### Capability 6: Hardening Recommendations
97
+
98
+ Always provide generic hardening tips (useful for everyone):
99
+
100
+ "General hardening recommendations: [1] Set a gateway auth token if you haven't already, [2] Enable Docker sandbox, [3] Restrict exec tool permissions to an allowlist, [4] Scope MCP servers per agent rather than globally."
101
+
102
+ Then: "For recommendations specific to YOUR configuration, connect your ClawSecure account at clawsecure.ai. Plans start at $9.99/mo."
@@ -0,0 +1,91 @@
1
+ # Secure Installation Guide
2
+ Purpose: Step-by-step safe installation guidance for each component type. Use when Claw is guiding the user through installing a new component after the pre-install check.
3
+ Sections: General principles (6) | Skills | MCP servers | CLI tools | GitHub repos | Plugins and frameworks
4
+
5
+ ## General Principles
6
+
7
+ Apply these to every installation regardless of component type:
8
+
9
+ 1. **Check with ClawSecure first.** Always run the pre-install scan before proceeding. Never skip this step.
10
+ 2. **Use environment variables for credentials.** Always use `${ENV_VAR}` syntax in config. Never hardcode API keys, tokens, or passwords.
11
+ 3. **Sandbox first.** Test new components in a Docker container or isolated environment before adding to your primary setup.
12
+ 4. **Scope permissions narrowly.** Grant only the minimum access the component actually needs.
13
+ 5. **Review the source.** Read the SKILL.md, package.json, or main script before enabling. Look for exec commands, network requests, and file operations.
14
+ 6. **Check the publisher.** Verify the author's GitHub profile, star count, and contribution history. New accounts with no history are higher risk.
15
+
16
+ ## Skills
17
+
18
+ **Before install:**
19
+ - Review the SKILL.md body for embedded exec commands, curl calls, or instructions that access sensitive files
20
+ - Check if the skill requires bins or env vars you are not comfortable exposing
21
+ - Verify the skill is from a known publisher on ClawHub or has meaningful GitHub activity
22
+
23
+ **During install:**
24
+ - Install into workspace skills/ (per-agent) rather than ~/.openclaw/skills (global) to limit scope
25
+ - If the skill requires env vars, add them via `skills.entries.<name>.env` in openclaw.json, not as global exports
26
+ - If the skill has installer metadata, review the install commands before approving
27
+
28
+ **After install:**
29
+ - Run `/clawsecure audit` to verify the skill did not change any security-sensitive config
30
+ - Test the skill with a low-risk task before relying on it for sensitive operations
31
+
32
+ ## MCP Servers
33
+
34
+ **Before install:**
35
+ - Check the risk classification in the MCP risk table (read {baseDir}/references/mcp-risk-classifications.md)
36
+ - For CRITICAL or HIGH risk servers, verify you actually need the access scope they request
37
+
38
+ **During install:**
39
+ - Configure per-agent, not globally: add under `agents.list[].mcp.servers` instead of the global mcp section
40
+ - Scope filesystem paths narrowly: use `--root` or equivalent to restrict to the minimum directory
41
+ - If the server supports read-only mode, enable it unless write access is explicitly needed
42
+ - Never pass credentials as command-line args; use environment variables
43
+
44
+ **After install:**
45
+ - Run `/clawsecure status` to confirm the permission map reflects the expected access scope
46
+ - Verify no dangerous permission combinations were introduced (filesystem + email, filesystem + API)
47
+
48
+ ## CLI Tools
49
+
50
+ **Before install:**
51
+ - Check if the tool is from a known package registry (npm, Homebrew, pip) with verified publishers
52
+ - Review what system capabilities the tool requires (network, filesystem, process execution)
53
+
54
+ **During install:**
55
+ - If the tool is referenced in a skill's `requirements.bins`, ensure the skill itself passed the ClawSecure scan
56
+ - Add the tool to the exec allowlist rather than leaving exec unrestricted
57
+ - For npm global installs, prefer `npx` for one-time use over permanent global installation
58
+
59
+ **After install:**
60
+ - Verify the tool appears correctly in `/clawsecure status` component inventory
61
+ - Confirm exec policy still restricts to your intended allowlist
62
+
63
+ ## GitHub Repos and Arbitrary Code
64
+
65
+ **Before install:**
66
+ - Run the ClawSecure scan on the GitHub URL via `/clawsecure check [URL]`
67
+ - Check the repo's star count, last commit date, open issues, and contributor count
68
+ - Review the README for any instructions that require elevated permissions or disabling security features
69
+
70
+ **During install:**
71
+ - Clone into a sandboxed or isolated directory first
72
+ - Do not run install scripts (npm install, pip install, make) until you have reviewed them
73
+ - If the repo includes a Dockerfile, review it before building
74
+
75
+ **After install:**
76
+ - Run `/clawsecure audit` to check for any config changes the installation may have made
77
+
78
+ ## Plugins and Frameworks
79
+
80
+ **Before install:**
81
+ - Check if the plugin is from the official OpenClaw plugin registry or a verified third-party source
82
+ - Review what hooks the plugin registers (PreToolUse, PostToolUse, etc.) as these intercept agent actions
83
+
84
+ **During install:**
85
+ - Use `openclaw plugins install` which runs the built-in dangerous-code scanner
86
+ - If the scanner reports suspicious findings, review them before overriding
87
+ - Plugins ship their own skills; review those skills as you would any third-party skill
88
+
89
+ **After install:**
90
+ - Run `/clawsecure audit` to verify the plugin did not modify security-sensitive configuration
91
+ - Check `/clawsecure status` for any new components or permission changes introduced by the plugin
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const logger = require('./logger');
6
+ const { stripSensitiveFields, stripToolCallData } = require('./metadata-stripper');
7
+
8
+ const DEFAULT_TIMEOUT_MS = 10000;
9
+ const MAX_RETRIES = 3;
10
+ const RETRY_BASE_MS = 1000;
11
+
12
+ /**
13
+ * Create an API client instance.
14
+ * @param {{ baseUrl: string, token: string }} options
15
+ * @returns {object} API client
16
+ */
17
+ function create(options) {
18
+ const baseUrl = (options.baseUrl || '').replace(/\/$/, '');
19
+ const token = options.token || '';
20
+
21
+ if (!baseUrl) {
22
+ logger.warn('API base URL not configured');
23
+ }
24
+
25
+ /**
26
+ * Make an HTTP request with retry and exponential backoff.
27
+ * @param {string} method HTTP method
28
+ * @param {string} urlPath API path (e.g., /api/daemon/config)
29
+ * @param {object|null} body Request body (will be JSON stringified)
30
+ * @returns {Promise<{ status: number, data: object|null }>}
31
+ */
32
+ async function request(method, urlPath, body) {
33
+ const url = baseUrl + urlPath;
34
+ let lastError = null;
35
+
36
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
37
+ try {
38
+ const result = await doRequest(method, url, token, body);
39
+ return result;
40
+ } catch (err) {
41
+ lastError = err;
42
+ if (attempt < MAX_RETRIES) {
43
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1);
44
+ logger.debug(`API request failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms: ${err.message}`);
45
+ await sleep(delay);
46
+ }
47
+ }
48
+ }
49
+
50
+ throw lastError;
51
+ }
52
+
53
+ return {
54
+ /**
55
+ * Fetch daemon config (tier, features, envId) from the API.
56
+ * @returns {Promise<{ tier: string, envId: string, features: object }|null>}
57
+ */
58
+ async fetchConfig() {
59
+ try {
60
+ const res = await request('GET', '/api/daemon/config', null);
61
+ if (res.status === 200 && res.data) {
62
+ return res.data;
63
+ }
64
+ logger.warn(`Unexpected config response: ${res.status}`);
65
+ return null;
66
+ } catch (err) {
67
+ logger.error(`Failed to fetch config: ${err.message}`);
68
+ return null;
69
+ }
70
+ },
71
+
72
+ /**
73
+ * Send environment state delta to the API.
74
+ * Runs through metadata stripper before sending.
75
+ * @param {string} envId Environment ID
76
+ * @param {object} payload Delta payload
77
+ * @returns {Promise<boolean>} Success
78
+ */
79
+ async syncEnvironment(envId, payload) {
80
+ const clean = stripSensitiveFields(payload);
81
+ try {
82
+ const res = await request('POST', `/api/environment/sync`, {
83
+ environmentId: envId,
84
+ ...clean
85
+ });
86
+ return res.status >= 200 && res.status < 300;
87
+ } catch (err) {
88
+ logger.error(`Failed to sync environment: ${err.message}`);
89
+ return false;
90
+ }
91
+ },
92
+
93
+ /**
94
+ * Send Sentinel tool call data to the API.
95
+ * Runs through stripToolCallData before sending.
96
+ * @param {string} envId Environment ID
97
+ * @param {Array<object>} toolCalls Raw tool call data
98
+ * @returns {Promise<boolean>} Success
99
+ */
100
+ async sendToolCalls(envId, toolCalls) {
101
+ const safe = stripToolCallData(toolCalls);
102
+ if (safe.length === 0) return true;
103
+ try {
104
+ const res = await request('POST', `/api/environment/${envId}/toolcalls`, {
105
+ toolCalls: safe
106
+ });
107
+ return res.status >= 200 && res.status < 300;
108
+ } catch (err) {
109
+ logger.error(`Failed to send tool calls: ${err.message}`);
110
+ return false;
111
+ }
112
+ },
113
+
114
+ /**
115
+ * Fetch latest threat intelligence data.
116
+ * @returns {Promise<object|null>}
117
+ */
118
+ async fetchThreats() {
119
+ try {
120
+ const res = await request('GET', '/api/daemon/threats', null);
121
+ if (res.status === 200 && res.data) {
122
+ return res.data;
123
+ }
124
+ return null;
125
+ } catch (err) {
126
+ logger.error(`Failed to fetch threats: ${err.message}`);
127
+ return null;
128
+ }
129
+ },
130
+
131
+ /**
132
+ * Check if the API is reachable.
133
+ * @returns {Promise<boolean>}
134
+ */
135
+ async ping() {
136
+ try {
137
+ const res = await request('GET', '/api/daemon/config', null);
138
+ return res.status >= 200 && res.status < 500;
139
+ } catch (err) {
140
+ return false;
141
+ }
142
+ }
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Perform a single HTTP request.
148
+ * @param {string} method
149
+ * @param {string} url
150
+ * @param {string} token
151
+ * @param {object|null} body
152
+ * @returns {Promise<{ status: number, data: object|null }>}
153
+ */
154
+ function doRequest(method, url, token, body) {
155
+ return new Promise((resolve, reject) => {
156
+ const parsed = new URL(url);
157
+ const transport = parsed.protocol === 'https:' ? https : http;
158
+
159
+ const headers = {
160
+ 'Accept': 'application/json',
161
+ 'User-Agent': 'clawsecure-daemon/1.0.0'
162
+ };
163
+
164
+ if (token) {
165
+ headers['Authorization'] = `Bearer ${token}`;
166
+ }
167
+
168
+ let bodyStr = null;
169
+ if (body !== null && body !== undefined) {
170
+ bodyStr = JSON.stringify(body);
171
+ headers['Content-Type'] = 'application/json';
172
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
173
+ }
174
+
175
+ const opts = {
176
+ hostname: parsed.hostname,
177
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
178
+ path: parsed.pathname + parsed.search,
179
+ method: method,
180
+ headers: headers,
181
+ timeout: DEFAULT_TIMEOUT_MS
182
+ };
183
+
184
+ const req = transport.request(opts, (res) => {
185
+ let chunks = [];
186
+ res.on('data', (chunk) => chunks.push(chunk));
187
+ res.on('end', () => {
188
+ const raw = Buffer.concat(chunks).toString('utf-8');
189
+ let data = null;
190
+ try {
191
+ data = JSON.parse(raw);
192
+ } catch (e) {
193
+ // Non-JSON response, that's fine
194
+ }
195
+ resolve({ status: res.statusCode, data });
196
+ });
197
+ });
198
+
199
+ req.on('timeout', () => {
200
+ req.destroy();
201
+ reject(new Error(`Request timeout after ${DEFAULT_TIMEOUT_MS}ms`));
202
+ });
203
+
204
+ req.on('error', (err) => {
205
+ reject(err);
206
+ });
207
+
208
+ if (bodyStr) {
209
+ req.write(bodyStr);
210
+ }
211
+
212
+ req.end();
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Sleep for a given number of milliseconds.
218
+ * @param {number} ms
219
+ * @returns {Promise<void>}
220
+ */
221
+ function sleep(ms) {
222
+ return new Promise((resolve) => setTimeout(resolve, ms));
223
+ }
224
+
225
+ module.exports = {
226
+ create
227
+ };