@sdxc/spec 0.0.0-pre.1

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.
Files changed (77) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +924 -0
  3. package/dist/ast.d.ts +193 -0
  4. package/dist/ast.js +9 -0
  5. package/dist/builtins.d.ts +29 -0
  6. package/dist/builtins.js +66 -0
  7. package/dist/cli.d.ts +21 -0
  8. package/dist/cli.js +297 -0
  9. package/dist/diagnostics.d.ts +47 -0
  10. package/dist/diagnostics.js +8 -0
  11. package/dist/errors.d.ts +131 -0
  12. package/dist/errors.js +159 -0
  13. package/dist/executor.d.ts +66 -0
  14. package/dist/executor.js +320 -0
  15. package/dist/expectation.d.ts +61 -0
  16. package/dist/expectation.js +222 -0
  17. package/dist/index.d.ts +51 -0
  18. package/dist/index.js +36 -0
  19. package/dist/lexer.d.ts +22 -0
  20. package/dist/lexer.js +284 -0
  21. package/dist/loader.d.ts +21 -0
  22. package/dist/loader.js +81 -0
  23. package/dist/parser.d.ts +24 -0
  24. package/dist/parser.js +502 -0
  25. package/dist/permissions.d.ts +139 -0
  26. package/dist/permissions.js +325 -0
  27. package/dist/plugin.d.ts +90 -0
  28. package/dist/plugin.js +9 -0
  29. package/dist/plugins/browser.d.ts +24 -0
  30. package/dist/plugins/browser.js +896 -0
  31. package/dist/plugins/cli.d.ts +17 -0
  32. package/dist/plugins/cli.js +134 -0
  33. package/dist/plugins/db-e2e-probe.d.ts +14 -0
  34. package/dist/plugins/db-e2e-probe.js +112 -0
  35. package/dist/plugins/db.d.ts +19 -0
  36. package/dist/plugins/db.js +199 -0
  37. package/dist/plugins/demo.d.ts +17 -0
  38. package/dist/plugins/demo.js +70 -0
  39. package/dist/plugins/env.d.ts +18 -0
  40. package/dist/plugins/env.js +87 -0
  41. package/dist/plugins/fs.d.ts +16 -0
  42. package/dist/plugins/fs.js +415 -0
  43. package/dist/plugins/http.d.ts +19 -0
  44. package/dist/plugins/http.js +505 -0
  45. package/dist/plugins/jwt.d.ts +17 -0
  46. package/dist/plugins/jwt.js +342 -0
  47. package/dist/plugins/sample.d.ts +27 -0
  48. package/dist/plugins/sample.js +400 -0
  49. package/dist/plugins/url.d.ts +18 -0
  50. package/dist/plugins/url.js +126 -0
  51. package/dist/project-config.d.ts +163 -0
  52. package/dist/project-config.js +497 -0
  53. package/dist/registry.d.ts +56 -0
  54. package/dist/registry.js +110 -0
  55. package/dist/reporter.d.ts +30 -0
  56. package/dist/reporter.js +237 -0
  57. package/dist/run.d.ts +74 -0
  58. package/dist/run.js +179 -0
  59. package/dist/runner.d.ts +52 -0
  60. package/dist/runner.js +38 -0
  61. package/dist/source.d.ts +37 -0
  62. package/dist/source.js +31 -0
  63. package/dist/sources.d.ts +45 -0
  64. package/dist/sources.js +54 -0
  65. package/dist/tokens.d.ts +34 -0
  66. package/dist/tokens.js +25 -0
  67. package/dist/transport-stdio.d.ts +34 -0
  68. package/dist/transport-stdio.js +400 -0
  69. package/dist/values.d.ts +48 -0
  70. package/dist/values.js +52 -0
  71. package/dist/workers.d.ts +40 -0
  72. package/dist/workers.js +26 -0
  73. package/dist/workspace-none.d.ts +23 -0
  74. package/dist/workspace-none.js +33 -0
  75. package/dist/workspace.d.ts +47 -0
  76. package/dist/workspace.js +116 -0
  77. package/package.json +28 -0
@@ -0,0 +1,505 @@
1
+ /**
2
+ * The built-in `http` plugin: `get`/`post`/`put`/`patch`/`delete` tools that
3
+ * issue real requests through the global fetch. Every tool requires the `net`
4
+ * grant, checked against the URL's host and port, and URLs must be absolute
5
+ * because v1 ships no environments mechanism to bind a base URL against. Beyond
6
+ * the URL, calls may carry word-tagged options — `headers { … }`, `form { … }`,
7
+ * `json …`, `text "…"`, and the auth shortcuts `bearer <token>` and
8
+ * `basic <user> <pass>` — in any order, alongside the back-compatible bare body.
9
+ *
10
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
11
+ * @copyright Sergio Xalambrí 2026
12
+ */
13
+ import { failure, isFailure, success } from "@sdxc/result";
14
+ import { ToolError } from "../errors.js";
15
+ /** The request tools the plugin exposes; each issues its uppercased method. */
16
+ const HTTP_VERBS = ["get", "post", "put", "patch", "delete"];
17
+ /**
18
+ * The words that tag an optional request argument. Each consumes the argument
19
+ * that follows it: `headers`/`form` an object, `json` any value, `text` a
20
+ * string.
21
+ */
22
+ const OPTION_WORDS = ["headers", "form", "json", "text"];
23
+ /**
24
+ * The words that tag an authentication shortcut, filling the `Authorization`
25
+ * header from author-provided values: `bearer` consumes one following value (the
26
+ * token), `basic` consumes two (the username and password).
27
+ */
28
+ const AUTH_WORDS = ["bearer", "basic"];
29
+ /** Every accepted option word, for the unknown-word diagnostic. */
30
+ const ALL_OPTION_WORDS = [...OPTION_WORDS, ...AUTH_WORDS];
31
+ /** How many redirect hops one request may follow before it is refused. */
32
+ const MAX_REDIRECTS = 10;
33
+ /**
34
+ * Create the built-in `http` plugin (namespace `"http"`). Tools take an
35
+ * absolute URL and optional body, check the `net` permission for the URL's
36
+ * host and port, then fetch; only network failures or misuse become errors.
37
+ */
38
+ export function createHttpPlugin() {
39
+ return {
40
+ namespace: "http",
41
+ describe() {
42
+ return HTTP_VERBS.map((verb) => describeVerb(verb));
43
+ },
44
+ async call(tool, args, context) {
45
+ if (!isVerb(tool)) {
46
+ return failure(new ToolError(`http has no tool "${tool}"; available tools: ${HTTP_VERBS.join(", ")}`));
47
+ }
48
+ return await request(tool, args, context);
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * Build the descriptor of one request tool: an action requiring the `net`
54
+ * grant, an absolute URL, an optional bare body, and the order-independent
55
+ * `headers`/`form`/`json`/`text`/`bearer`/`basic` word-tagged options.
56
+ */
57
+ function describeVerb(verb) {
58
+ return {
59
+ name: verb,
60
+ summary: `Send an HTTP ${verb.toUpperCase()} request to an absolute URL.`,
61
+ kind: "action",
62
+ requires: "net",
63
+ params: [
64
+ {
65
+ name: "url",
66
+ kind: "value",
67
+ required: true,
68
+ summary: "Absolute URL of the request; v1 has no base-URL binding.",
69
+ },
70
+ {
71
+ name: "body",
72
+ kind: "value",
73
+ required: false,
74
+ summary: "Optional bare body: a string is sent as text/plain, any other value as JSON.",
75
+ },
76
+ {
77
+ name: "headers",
78
+ kind: "word",
79
+ required: false,
80
+ summary: "Tag before an object of header name/value pairs; an explicit content-type overrides the body's.",
81
+ },
82
+ {
83
+ name: "form",
84
+ kind: "word",
85
+ required: false,
86
+ summary: "Tag before an object sent as an application/x-www-form-urlencoded body.",
87
+ },
88
+ {
89
+ name: "json",
90
+ kind: "word",
91
+ required: false,
92
+ summary: "Tag before any value sent as an application/json body.",
93
+ },
94
+ {
95
+ name: "text",
96
+ kind: "word",
97
+ required: false,
98
+ summary: "Tag before a string sent as a text/plain body.",
99
+ },
100
+ {
101
+ name: "bearer",
102
+ kind: "word",
103
+ required: false,
104
+ summary: "Tag before a token string, sent as an Authorization: Bearer header.",
105
+ },
106
+ {
107
+ name: "basic",
108
+ kind: "word",
109
+ required: false,
110
+ summary: "Tag before a username and a password, sent as an Authorization: Basic header.",
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ /** Narrow a tool name to one of the plugin's request verbs. */
116
+ function isVerb(tool) {
117
+ return HTTP_VERBS.includes(tool);
118
+ }
119
+ /**
120
+ * Run one request tool end to end: validate the arguments, reject a body on
121
+ * GET, and encode the request before checking the `net` permission — that
122
+ * check gates last, so a malformed call never reaches the network.
123
+ */
124
+ async function request(verb, args, context) {
125
+ let parsedArgs = readArgs(verb, args);
126
+ if (isFailure(parsedArgs))
127
+ return parsedArgs;
128
+ if (parsedArgs.data.body !== undefined && verb === "get") {
129
+ return failure(new ToolError(`http.get cannot send a request body; a GET request carries no body`));
130
+ }
131
+ let target = parseTarget(verb, parsedArgs.data.url);
132
+ if (isFailure(target))
133
+ return target;
134
+ let init = buildInit(verb, parsedArgs.data.body, parsedArgs.data.headers, parsedArgs.data.auth);
135
+ if (isFailure(init))
136
+ return init;
137
+ let allowed = context.permissions.checkNet(target.data.hostname, portOf(target.data));
138
+ if (isFailure(allowed))
139
+ return allowed;
140
+ return await perform(verb, target.data, init.data, context.permissions);
141
+ }
142
+ /**
143
+ * Validate the raw tool arguments into a URL, an optional body, headers, and
144
+ * an auth credential. Word-tagged options and the auth shortcuts may follow
145
+ * in any order, but each of body, `headers`, and auth is accepted once.
146
+ *
147
+ * @throws {ToolError} on a missing/non-string URL, an unknown option word, a
148
+ * second body, `headers` block, or auth option, or a word missing its
149
+ * value(s).
150
+ */
151
+ function readArgs(verb, args) {
152
+ let first = args[0];
153
+ if (first === undefined) {
154
+ return failure(new ToolError(`http.${verb} requires a URL; got no arguments`));
155
+ }
156
+ if (first.kind !== "value" || typeof first.value !== "string") {
157
+ return failure(new ToolError(`http.${verb} requires its first argument to be a URL string`));
158
+ }
159
+ let url = first.value;
160
+ let body;
161
+ let headers;
162
+ let auth;
163
+ let index = 1;
164
+ while (index < args.length) {
165
+ let arg = args[index];
166
+ if (arg === undefined)
167
+ break;
168
+ if (arg.kind === "word") {
169
+ let word = arg.word;
170
+ if (isAuthWord(word)) {
171
+ if (auth !== undefined) {
172
+ return failure(new ToolError(`http.${verb} accepts at most one auth option (bearer or basic), but got more than one`));
173
+ }
174
+ let parsed = readAuth(verb, word, args, index);
175
+ if (isFailure(parsed))
176
+ return parsed;
177
+ auth = parsed.data.auth;
178
+ index += parsed.data.consumed;
179
+ continue;
180
+ }
181
+ if (!isOptionWord(word)) {
182
+ return failure(new ToolError(`http.${verb} got the unknown option word "${word}"; expected one of ${ALL_OPTION_WORDS.join(", ")}`));
183
+ }
184
+ let next = args[index + 1];
185
+ if (next === undefined || next.kind !== "value") {
186
+ return failure(new ToolError(`http.${verb} option "${word}" needs a value argument after it`));
187
+ }
188
+ if (word === "headers") {
189
+ if (headers !== undefined) {
190
+ return failure(new ToolError(`http.${verb} accepts at most one headers block`));
191
+ }
192
+ headers = next.value;
193
+ }
194
+ else {
195
+ let incoming = { kind: word, value: next.value, source: word };
196
+ if (body !== undefined)
197
+ return failure(twoBodies(verb, body, incoming));
198
+ body = incoming;
199
+ }
200
+ index += 2;
201
+ continue;
202
+ }
203
+ let incoming = typeof arg.value === "string"
204
+ ? { kind: "text", value: arg.value, source: "bare" }
205
+ : { kind: "json", value: arg.value, source: "bare" };
206
+ if (body !== undefined)
207
+ return failure(twoBodies(verb, body, incoming));
208
+ body = incoming;
209
+ index += 1;
210
+ }
211
+ return success({ url, body, headers, auth });
212
+ }
213
+ /**
214
+ * Read a `bearer` or `basic` auth option starting at its tag: `bearer`
215
+ * consumes one following string (the token), `basic` two (the username and
216
+ * password), reporting how many arguments, tag included, it consumed.
217
+ *
218
+ * @throws {ToolError} when a credential value is missing or not a string.
219
+ */
220
+ function readAuth(verb, word, args, index) {
221
+ if (word === "bearer") {
222
+ let token = authString(verb, "bearer", args[index + 1], "token");
223
+ if (isFailure(token))
224
+ return token;
225
+ return success({ auth: { kind: "bearer", token: token.data }, consumed: 2 });
226
+ }
227
+ let user = authString(verb, "basic", args[index + 1], "username");
228
+ if (isFailure(user))
229
+ return user;
230
+ let pass = authString(verb, "basic", args[index + 2], "password");
231
+ if (isFailure(pass))
232
+ return pass;
233
+ return success({ auth: { kind: "basic", user: user.data, pass: pass.data }, consumed: 3 });
234
+ }
235
+ /** Read one auth credential argument as a required string, or a tool error. */
236
+ function authString(verb, word, arg, role) {
237
+ if (arg === undefined || arg.kind !== "value" || typeof arg.value !== "string") {
238
+ return failure(new ToolError(`http.${verb} option "${word}" needs a ${role} string argument after it`));
239
+ }
240
+ return success(arg.value);
241
+ }
242
+ /** Narrow a bare-word argument to one of the option tags. */
243
+ function isOptionWord(word) {
244
+ return OPTION_WORDS.includes(word);
245
+ }
246
+ /** Narrow a bare-word argument to one of the auth tags. */
247
+ function isAuthWord(word) {
248
+ return AUTH_WORDS.includes(word);
249
+ }
250
+ /** The tool error raised when a call supplies more than one request body. */
251
+ function twoBodies(verb, existing, incoming) {
252
+ return new ToolError(`http.${verb} accepts one request body, but got ${bodyLabel(existing.source)} and ${bodyLabel(incoming.source)}`);
253
+ }
254
+ /** How a body reads in a conflict message: the bare body, or a tagged one. */
255
+ function bodyLabel(source) {
256
+ return source === "bare" ? "a bare body" : `a \`${source}\` body`;
257
+ }
258
+ /**
259
+ * Parse the spec-written URL, requiring an absolute http(s) URL. Relative
260
+ * URLs are refused with the v1 rationale: there is no environments mechanism
261
+ * to bind a base URL against yet.
262
+ */
263
+ function parseTarget(verb, raw) {
264
+ let url;
265
+ try {
266
+ url = new URL(raw);
267
+ }
268
+ catch {
269
+ return failure(new ToolError(`http.${verb} received the relative URL "${raw}"; v1 has no environments mechanism to bind a base URL against, so URLs must be absolute (see docs/adr/spec/ADR-008-environments-and-compatibility.md)`));
270
+ }
271
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
272
+ return failure(new ToolError(`http.${verb} supports absolute http(s) URLs only; got "${raw}"`));
273
+ }
274
+ return success(url);
275
+ }
276
+ /** The port the request will reach: the URL's own, or the scheme default (80/443). */
277
+ function portOf(url) {
278
+ if (url.port !== "")
279
+ return Number(url.port);
280
+ return url.protocol === "https:" ? 443 : 80;
281
+ }
282
+ /**
283
+ * Perform the fetch, following redirects by hand rather than through fetch's
284
+ * own handling, so each redirect target passes the same `net` check as the
285
+ * original URL before any request reaches it.
286
+ *
287
+ * @returns Success shaped as `{ status, ok, headers, text, json }` from the
288
+ * final response, or a permission-denied or tool-error failure.
289
+ */
290
+ async function perform(verb, url, init, permissions) {
291
+ let current = url;
292
+ for (let redirects = 0;; redirects++) {
293
+ let response;
294
+ try {
295
+ response = await fetch(current, { ...init, redirect: "manual" });
296
+ }
297
+ catch (error) {
298
+ return failure(new ToolError(`http.${verb} request to ${current.href} failed: ${describeFailure(error)}`));
299
+ }
300
+ let location = response.headers.get("location");
301
+ if (!isRedirectStatus(response.status) || location === null) {
302
+ return await shapeResponse(verb, current, response);
303
+ }
304
+ if (redirects >= MAX_REDIRECTS) {
305
+ return failure(new ToolError(`http.${verb} request to ${url.href} followed more than ${MAX_REDIRECTS} redirects`));
306
+ }
307
+ let next = parseLocation(verb, location, current);
308
+ if (isFailure(next))
309
+ return next;
310
+ let allowed = permissions.checkNet(next.data.hostname, portOf(next.data));
311
+ if (isFailure(allowed))
312
+ return allowed;
313
+ init = redirectInit(init, response.status, current, next.data);
314
+ current = next.data;
315
+ }
316
+ }
317
+ /**
318
+ * Shape one final (non-redirect) response into the tool's result value.
319
+ */
320
+ async function shapeResponse(verb, url, response) {
321
+ let text;
322
+ try {
323
+ text = await response.text();
324
+ }
325
+ catch (error) {
326
+ return failure(new ToolError(`http.${verb} request to ${url.href} failed: ${describeFailure(error)}`));
327
+ }
328
+ let headers = {};
329
+ for (let [name, value] of response.headers)
330
+ headers[name.toLowerCase()] = value;
331
+ return success({
332
+ status: response.status,
333
+ ok: response.ok,
334
+ headers,
335
+ text,
336
+ json: parseJson(text),
337
+ });
338
+ }
339
+ /** The redirect statuses a default fetch would transparently follow. */
340
+ function isRedirectStatus(status) {
341
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
342
+ }
343
+ /**
344
+ * Resolve a `Location` header against the URL that sent it, requiring the
345
+ * result to stay an http(s) URL.
346
+ */
347
+ function parseLocation(verb, location, base) {
348
+ let url;
349
+ try {
350
+ url = new URL(location, base);
351
+ }
352
+ catch {
353
+ return failure(new ToolError(`http.${verb} received an unparsable redirect Location: "${location}"`));
354
+ }
355
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
356
+ return failure(new ToolError(`http.${verb} supports absolute http(s) URLs only; a redirect pointed to "${url.href}"`));
357
+ }
358
+ return success(url);
359
+ }
360
+ /** Credential headers the fetch standard strips on a cross-origin redirect. */
361
+ const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
362
+ /**
363
+ * The init for the next hop, per the fetch standard's method rewrite: 303,
364
+ * or a non-GET 301/302, switches to GET and drops the body; other statuses
365
+ * keep the init but strip credential headers when the hop crosses origins.
366
+ */
367
+ function redirectInit(init, status, from, to) {
368
+ if (status === 303 || ((status === 301 || status === 302) && init.method !== "GET")) {
369
+ return { method: "GET" };
370
+ }
371
+ if (from.origin === to.origin)
372
+ return init;
373
+ return stripCredentialHeaders(init);
374
+ }
375
+ /**
376
+ * Drop the credential headers the fetch standard removes on a cross-origin
377
+ * redirect. The prebuilt init's header names are already lowercased, but the
378
+ * comparison lowercases too so the guard holds regardless.
379
+ */
380
+ function stripCredentialHeaders(init) {
381
+ if (init.headers === undefined)
382
+ return init;
383
+ let kept = {};
384
+ for (let [name, value] of Object.entries(init.headers)) {
385
+ if (!CROSS_ORIGIN_STRIPPED_HEADERS.includes(name.toLowerCase()))
386
+ kept[name] = value;
387
+ }
388
+ if (Object.keys(kept).length === Object.keys(init.headers).length) {
389
+ return init;
390
+ }
391
+ let { headers: _stripped, ...rest } = init;
392
+ return Object.keys(kept).length === 0 ? rest : { ...rest, headers: kept };
393
+ }
394
+ /**
395
+ * Build the fetch init for a verb, optional body, `bearer`/`basic`
396
+ * credential, and `headers`, layered so an explicit `content-type` overrides
397
+ * the body's default and an explicit `authorization` overrides `bearer`/`basic`.
398
+ */
399
+ function buildInit(verb, body, headers, auth) {
400
+ let encoded = encodeBody(verb, body);
401
+ if (isFailure(encoded))
402
+ return encoded;
403
+ let finalHeaders = {};
404
+ if (encoded.data.contentType !== undefined) {
405
+ finalHeaders["content-type"] = encoded.data.contentType;
406
+ }
407
+ if (auth !== undefined) {
408
+ let authorization = authorizationHeader(verb, auth);
409
+ if (isFailure(authorization))
410
+ return authorization;
411
+ finalHeaders["authorization"] = authorization.data;
412
+ }
413
+ if (headers !== undefined) {
414
+ let coerced = coerceFields(verb, "headers", headers);
415
+ if (isFailure(coerced))
416
+ return coerced;
417
+ for (let [name, value] of Object.entries(coerced.data)) {
418
+ finalHeaders[name.toLowerCase()] = value;
419
+ }
420
+ }
421
+ let init = { method: verb.toUpperCase() };
422
+ if (encoded.data.body !== undefined)
423
+ init.body = encoded.data.body;
424
+ if (Object.keys(finalHeaders).length > 0)
425
+ init.headers = finalHeaders;
426
+ return success(init);
427
+ }
428
+ /**
429
+ * Render an auth spec into its `Authorization` header value: `bearer`
430
+ * becomes `Bearer <token>`; `basic` becomes `Basic <base64(user:pass)>` per
431
+ * RFC 7617, reporting a credential outside Latin-1 as a tool error.
432
+ */
433
+ function authorizationHeader(verb, auth) {
434
+ if (auth.kind === "bearer")
435
+ return success(`Bearer ${auth.token}`);
436
+ try {
437
+ return success(`Basic ${btoa(`${auth.user}:${auth.pass}`)}`);
438
+ }
439
+ catch {
440
+ return failure(new ToolError(`http.${verb} basic credentials must be Latin-1 (base64-encodable); got a value outside that range`));
441
+ }
442
+ }
443
+ /**
444
+ * Serialize a body spec into its wire string and default content type:
445
+ * `text` verbatim as text/plain, `form` urlencoded via URLSearchParams as
446
+ * application/x-www-form-urlencoded, and `json` as JSON of any value.
447
+ *
448
+ * @throws {ToolError} when a `text` body value is not a string.
449
+ */
450
+ function encodeBody(verb, body) {
451
+ if (body === undefined)
452
+ return success({ body: undefined, contentType: undefined });
453
+ if (body.kind === "text") {
454
+ if (typeof body.value !== "string") {
455
+ return failure(new ToolError(`http.${verb} text body must be a string`));
456
+ }
457
+ return success({ body: body.value, contentType: "text/plain" });
458
+ }
459
+ if (body.kind === "form") {
460
+ let coerced = coerceFields(verb, "form", body.value);
461
+ if (isFailure(coerced))
462
+ return coerced;
463
+ return success({
464
+ body: new URLSearchParams(coerced.data).toString(),
465
+ contentType: "application/x-www-form-urlencoded",
466
+ });
467
+ }
468
+ return success({ body: JSON.stringify(body.value), contentType: "application/json" });
469
+ }
470
+ /**
471
+ * Coerce a `headers` or `form` object into a string map: string values pass
472
+ * through, numbers and booleans stringify, and a non-object container or a
473
+ * null/array/object field value is a tool error naming the offending field.
474
+ */
475
+ function coerceFields(verb, label, value) {
476
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
477
+ return failure(new ToolError(`http.${verb} ${label} must be an object of string values`));
478
+ }
479
+ let fields = {};
480
+ for (let [key, raw] of Object.entries(value)) {
481
+ if (typeof raw === "string")
482
+ fields[key] = raw;
483
+ else if (typeof raw === "number" || typeof raw === "boolean")
484
+ fields[key] = String(raw);
485
+ else {
486
+ return failure(new ToolError(`http.${verb} ${label} field "${key}" must be a string, number, or boolean`));
487
+ }
488
+ }
489
+ return success(fields);
490
+ }
491
+ /** Parse a response body as JSON, yielding null when it is not valid JSON. */
492
+ function parseJson(text) {
493
+ try {
494
+ return JSON.parse(text);
495
+ }
496
+ catch {
497
+ return null;
498
+ }
499
+ }
500
+ /** Render an unknown thrown value into a one-line failure description. */
501
+ function describeFailure(error) {
502
+ if (error instanceof Error)
503
+ return error.message;
504
+ return String(error);
505
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The built-in `jwt` capability: read and verify JSON Web Tokens for OIDC.
3
+ * `jwt.decode` splits a token with no signature check, permissionless.
4
+ * `jwt.verify` fetches the JWKS, selects the named key, and checks the
5
+ * ES256 signature and expiry with WebCrypto, declaring `net`. Requiring
6
+ * ES256 alone closes the "alg confusion" downgrade class of attack.
7
+ *
8
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
9
+ * @copyright Sergio Xalambrí 2026
10
+ */
11
+ import type { Plugin } from "../plugin.js";
12
+ /**
13
+ * Create the built-in `jwt` plugin (namespace `"jwt"`): `jwt.decode` (a
14
+ * permissionless observable) and `jwt.verify` (a `net` action), each
15
+ * returning a {@link ToolError} for a malformed, unverifiable, or expired token.
16
+ */
17
+ export declare function createJwtPlugin(): Plugin;