1use std::collections::BTreeMap;
5
6use crate::{
7 DefaultValue, Dispatch, MethodSig, PackageDeclaration, Param, PropertySig, Span, Type,
8 TypeKind, TypeSymbol, ValueKind, ValueSymbol,
9};
10
11use super::MODULE_NAME;
12
13pub fn package_declaration() -> PackageDeclaration {
14 let mut defs = PackageDeclaration::with_package(MODULE_NAME);
15
16 defs.types.insert(
17 "Headers".to_string(),
18 TypeSymbol {
19 name: "Headers".to_string(),
20 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
21 declaration_span: Span::at(crate::FileId::HTTP),
22 kind: TypeKind::Alias {
23 generics: Vec::new(),
24 ty: Type::InterfaceRef { mangled: crate::mangle::prelude("Map"),
25 package: crate::Package::prelude(),
26 name: "Map".to_string(),
27 args: vec![Type::String, Type::String],
28 },
29 doc: crate::doc(crate::FileId::HTTP,
30 "/** HTTP header bag: `Map<string, string>` with names lowercased on the wire. Future slice may add case-insensitive lookup. */",
31 ),
32 },
33 },
34 );
35
36 insert_response_interface(&mut defs);
37 insert_download_options_interface(&mut defs);
38 insert_download_result_interface(&mut defs);
39
40 for verb in &["get", "delete", "head", "options"] {
42 insert_verb_fn(&mut defs, verb, false);
43 }
44 for verb in &["post", "put", "patch"] {
45 insert_verb_fn(&mut defs, verb, true);
46 }
47
48 defs.values.insert(
49 "download".to_string(),
50 ValueSymbol {
51 name: "download".to_string(),
52 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "download"),
53 declaration_span: Span::at(crate::FileId::HTTP),
54 kind: ValueKind::Function {
55 generics: Vec::new(),
56 params: vec![
57 Param::new("url", Type::String),
58 Param::new("path", Type::String),
59 Param::with_default(
60 "options",
61 optional_download_options_type(),
62 DefaultValue::Undefined,
63 ),
64 ],
65 ret: download_result_type(),
66 type_predicate: None,
67 doc: crate::doc(crate::FileId::HTTP,
68 "/**\n * Download a remote file directly to the VFS — wget-style. Streams the response body to disk so memory stays bounded regardless of file size. Atomic: a crash mid-download leaves a `.tmp` sibling rather than a half-written final file. Traps on transport failure (DNS, connect, TLS, timeout), oversize response, refuse-on-exists, or VFS path escape.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param path Destination VFS path (e.g. `\"/workspace/dataset.csv\"`). The parent directory must exist — call `fs.mkdir` first if needed.\n * @param options Optional `DownloadOptions` bag: `overwrite` (default `false`), `maxBytes` (default and upper bound: tier-configured), `headers`, `timeout` (default and upper bound: 60_000 ms, or a lower operator limit), `decompress` (default `false`). Omit or pass `undefined` to take every default.\n * @capability http.download { host: $url.host, url_path: $url.path, vfs_path: $path, max_bytes: number, overwrite: boolean, decompress: boolean }\n * @capability fs.write { path, max_bytes: number }\n */",
69 ),
70 },
71 },
72 );
73
74 defs.values.insert(
75 "request".to_string(),
76 ValueSymbol {
77 name: "request".to_string(),
78 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "request"),
79 declaration_span: Span::at(crate::FileId::HTTP),
80 kind: ValueKind::Function {
81 generics: Vec::new(),
82 params: vec![
83 Param::new("method", Type::String),
84 Param::new("url", Type::String),
85 Param::with_default(
86 "body",
87 optional_body_type(),
88 DefaultValue::Undefined,
89 ),
90 Param::with_default(
91 "headers",
92 optional_headers_type(),
93 DefaultValue::Undefined,
94 ),
95 ],
96 ret: response_type(),
97 type_predicate: None,
98 doc: crate::doc(crate::FileId::HTTP,
99 "/**\n * Issue an HTTP request with a runtime-chosen verb. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param method HTTP method (case-insensitive). Common verbs: `\"GET\"` / `\"POST\"` / `\"PUT\"` / `\"PATCH\"` / `\"DELETE\"` / `\"HEAD\"` / `\"OPTIONS\"`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n * @capability http.get { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.post { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.put { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.patch { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.delete { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.head { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.options { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n */",
100 ),
101 },
102 },
103 );
104
105 defs
106}
107
108fn insert_verb_fn(defs: &mut PackageDeclaration, verb: &str, has_body: bool) {
109 let upper = verb.to_uppercase();
110 let doc = if has_body {
111 format!(
112 "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
113 )
114 } else {
115 format!(
116 "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
117 )
118 };
119 let capability = format!(
120 " * @capability http.{verb} {{ host: $url.host, path: $url.path, body_size: number, timeout_ms: number }}\n */"
121 );
122 let doc = doc.replace(" */", &capability);
123 let mut params = vec![Param::new("url", Type::String)];
124 if has_body {
125 params.push(Param::with_default(
126 "body",
127 optional_body_type(),
128 DefaultValue::Undefined,
129 ));
130 }
131 params.push(Param::with_default(
132 "headers",
133 optional_headers_type(),
134 DefaultValue::Undefined,
135 ));
136 defs.values.insert(
137 verb.to_string(),
138 ValueSymbol {
139 name: verb.to_string(),
140 mangled_name: crate::mangle::package_symbol(MODULE_NAME, verb),
141 declaration_span: Span::at(crate::FileId::HTTP),
142 kind: ValueKind::Function {
143 generics: Vec::new(),
144 params,
145 ret: response_type(),
146 type_predicate: None,
147 doc: crate::doc(crate::FileId::HTTP, &doc),
148 },
149 },
150 );
151}
152
153fn optional_headers_type() -> Type {
154 Type::Union(vec![headers_type(), Type::Undefined])
155}
156
157fn optional_body_type() -> Type {
158 Type::Union(vec![
159 Type::String,
160 Type::Uint8Array,
161 Type::Object {
165 index: None,
166 fields: BTreeMap::new(),
167 },
168 Type::Array(Box::new(Type::Unknown)),
169 Type::Null,
170 Type::Undefined,
171 ])
172}
173
174fn response_type() -> Type {
175 Type::InterfaceRef {
176 mangled: crate::mangle::package_symbol(MODULE_NAME, "Response"),
177 package: crate::Package(MODULE_NAME.to_string()),
178 name: "Response".to_string(),
179 args: Vec::new(),
180 }
181}
182
183fn download_result_type() -> Type {
184 Type::InterfaceRef {
185 mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
186 package: crate::Package(MODULE_NAME.to_string()),
187 name: "DownloadResult".to_string(),
188 args: Vec::new(),
189 }
190}
191
192fn download_options_type() -> Type {
193 Type::InterfaceRef {
194 mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
195 package: crate::Package(MODULE_NAME.to_string()),
196 name: "DownloadOptions".to_string(),
197 args: Vec::new(),
198 }
199}
200
201fn optional_download_options_type() -> Type {
202 Type::Union(vec![download_options_type(), Type::Undefined])
203}
204
205fn headers_type() -> Type {
206 Type::Alias {
207 mangled: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
208 package: crate::Package(MODULE_NAME.to_string()),
209 name: "Headers".to_string(),
210 args: Vec::new(),
211 ty: Box::new(Type::InterfaceRef {
212 mangled: crate::mangle::prelude("Map"),
213 package: crate::Package::prelude(),
214 name: "Map".to_string(),
215 args: vec![Type::String, Type::String],
216 }),
217 }
218}
219
220fn insert_response_interface(defs: &mut PackageDeclaration) {
221 let mut properties = BTreeMap::new();
222 insert_property(
223 &mut properties,
224 "body",
225 Type::String,
226 "/** Response body, decoded as UTF-8. For binary responses, a future `bytes()` method will return `Uint8Array` without lossy decoding. */",
227 );
228 insert_property(
229 &mut properties,
230 "headers",
231 headers_type(),
232 "/** Response headers. Keys lowercased on the wire so `headers.get(\"content-type\")` works regardless of how the server cased them. */",
233 );
234 insert_property(
235 &mut properties,
236 "ok",
237 Type::Boolean,
238 "/** `true` iff `status` is in the 2xx range. */",
239 );
240 insert_property(
241 &mut properties,
242 "status",
243 Type::Number,
244 "/** HTTP status code (e.g. `200`, `404`). */",
245 );
246 insert_property(
247 &mut properties,
248 "statusText",
249 Type::String,
250 "/** Reason phrase (e.g. `\"OK\"`, `\"Not Found\"`). Empty when the server doesn't supply one. */",
251 );
252 insert_property(
253 &mut properties,
254 "url",
255 Type::String,
256 "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
257 );
258
259 let mut methods = BTreeMap::new();
260 methods.insert(
261 "throwForStatus".to_string(),
262 MethodSig {
263 optional: false,
264 generics: Vec::new(),
265 params: Vec::new(),
266 ret: Type::Void,
267 predicate: None,
268 doc: crate::doc(crate::FileId::HTTP,
269 "/** Throw if `ok` is `false`. The error message includes the status code, reason phrase, and final URL; catch it with `try`/`catch` like any other `Error`. */",
270 ),
271 },
272 );
273 methods.insert(
274 "toString".to_string(),
275 MethodSig {
276 optional: false,
277 generics: Vec::new(),
278 params: Vec::new(),
279 ret: Type::String,
280 predicate: None,
281 doc: crate::doc(crate::FileId::HTTP,
282 "/** Concise description of the response — `\"Response(200 OK, https://...)\"`. Built host-side for efficiency. */",
283 ),
284 },
285 );
286 methods.insert(
290 "json".to_string(),
291 MethodSig {
292 optional: false,
293 generics: Vec::new(),
294 params: Vec::new(),
295 ret: Type::Unknown,
296 predicate: None,
297 doc: crate::doc(crate::FileId::HTTP,
298 "/** Parse the response body as JSON into `unknown`. Validate with `as`, for example `r.json() as User`. */",
299 ),
300 },
301 );
302
303 defs.types.insert(
304 "Response".to_string(),
305 TypeSymbol {
306 name: "Response".to_string(),
307 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Response"),
308 declaration_span: Span::at(crate::FileId::HTTP),
309 kind: TypeKind::Interface { index: None,
310 generics: Vec::new(),
311 methods,
312 properties,
313 dispatch: Dispatch::Direct,
314 doc: crate::doc(crate::FileId::HTTP,
315 "/** Result of an HTTP call — status, headers, body, and the final URL after any redirects. Constructed only by the verb-form helpers; user code reads fields via property access and calls `throwForStatus()` / `toString()`. */",
316 ),
317 },
318 },
319 );
320}
321
322fn insert_download_options_interface(defs: &mut PackageDeclaration) {
323 let mut properties = BTreeMap::new();
324 insert_optional_property(
325 &mut properties,
326 "overwrite",
327 Type::Boolean,
328 "/** When `true`, clobber an existing file at `path`. Default `false` — refuse with a trap, since LLMs retry operations and silent overwrites cause data loss. */",
329 );
330 insert_optional_property(
331 &mut properties,
332 "maxBytes",
333 Type::Number,
334 "/** Maximum bytes to write to disk (post-decompression when `decompress` is true). Default: the tier-configured global limit. The download traps with a `response too large` error if exceeded. */",
335 );
336 insert_optional_property(
337 &mut properties,
338 "headers",
339 optional_headers_type(),
340 "/** Request headers — typically used for auth (`Authorization: Bearer …`). */",
341 );
342 insert_optional_property(
343 &mut properties,
344 "timeout",
345 Type::Number,
346 "/** Per-request timeout in milliseconds. Default 60_000 (vs 30_000 for the verb-form helpers — downloads are usually larger). */",
347 );
348 insert_optional_property(
349 &mut properties,
350 "decompress",
351 Type::Boolean,
352 "/** When `true`, transparently gunzip / unzstd the response if the `Content-Encoding` header (or URL suffix `.gz` / `.zst`) advertises a compressed stream. Default `false`: downloading `data.csv.gz` saves the `.gz` file as-is. */",
353 );
354
355 defs.types.insert(
356 "DownloadOptions".to_string(),
357 TypeSymbol {
358 name: "DownloadOptions".to_string(),
359 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
360 declaration_span: Span::at(crate::FileId::HTTP),
361 kind: TypeKind::Interface { index: None,
362 generics: Vec::new(),
363 methods: BTreeMap::new(),
364 properties,
365 dispatch: Dispatch::VTable,
367 doc: crate::doc(crate::FileId::HTTP,
368 "/** Options bag for [`download`]. All fields optional. Pass `undefined` (or omit the param) to take every default. */",
369 ),
370 },
371 },
372 );
373}
374
375fn insert_optional_property(
376 properties: &mut BTreeMap<String, PropertySig>,
377 name: &str,
378 ty: Type,
379 doc: &str,
380) {
381 properties.insert(
382 name.to_string(),
383 PropertySig {
384 ty,
385 readonly: false,
386 intrinsic: false,
387 optional: true,
388 doc: crate::doc(crate::FileId::HTTP, doc),
389 },
390 );
391}
392
393fn insert_download_result_interface(defs: &mut PackageDeclaration) {
394 let mut properties = BTreeMap::new();
395 insert_property(
396 &mut properties,
397 "bytesWritten",
398 Type::Number,
399 "/** Bytes actually written to disk (post-decompression when `decompress` was set). */",
400 );
401 insert_property(
402 &mut properties,
403 "contentType",
404 Type::String,
405 "/** `Content-Type` header from the response, lowercased. Empty string when the server didn't send one. */",
406 );
407 insert_property(
408 &mut properties,
409 "duration_ms",
410 Type::Number,
411 "/** Wall-clock duration of the download in milliseconds, measured from temp-file open through atomic rename. */",
412 );
413 insert_property(
414 &mut properties,
415 "finalUrl",
416 Type::String,
417 "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
418 );
419 insert_property(
420 &mut properties,
421 "path",
422 Type::String,
423 "/** The VFS path the file was written to — echoes the `path` argument. */",
424 );
425 insert_property(
426 &mut properties,
427 "status",
428 Type::Number,
429 "/** HTTP status code (e.g. `200`). The download does not trap on non-2xx — a 404 still writes the response body to disk, on the theory that an HTML error page is sometimes what an agent wants. Check `status` to discriminate. */",
430 );
431
432 let mut methods = BTreeMap::new();
433 methods.insert(
434 "toString".to_string(),
435 MethodSig {
436 optional: false,
437 generics: Vec::new(),
438 params: Vec::new(),
439 ret: Type::String,
440 predicate: None,
441 doc: crate::doc(crate::FileId::HTTP,
442 "/** Concise description — `\"Download(200, 1024 bytes -> /workspace/file.csv)\"`. */",
443 ),
444 },
445 );
446
447 defs.types.insert(
448 "DownloadResult".to_string(),
449 TypeSymbol {
450 name: "DownloadResult".to_string(),
451 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
452 declaration_span: Span::at(crate::FileId::HTTP),
453 kind: TypeKind::Interface { index: None,
454 generics: Vec::new(),
455 methods,
456 properties,
457 dispatch: Dispatch::Direct,
458 doc: crate::doc(crate::FileId::HTTP,
459 "/** Result of [`download`] — what was written, where, and the response metadata. */",
460 ),
461 },
462 },
463 );
464}
465
466fn insert_property(
467 properties: &mut BTreeMap<String, PropertySig>,
468 name: &str,
469 ty: Type,
470 doc: &str,
471) {
472 properties.insert(
473 name.to_string(),
474 PropertySig {
475 ty,
476 readonly: true,
477 intrinsic: false,
478 optional: false,
479 doc: crate::doc(crate::FileId::HTTP, doc),
480 },
481 );
482}