@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.
- package/LICENSE.md +21 -0
- package/README.md +924 -0
- package/dist/ast.d.ts +193 -0
- package/dist/ast.js +9 -0
- package/dist/builtins.d.ts +29 -0
- package/dist/builtins.js +66 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +297 -0
- package/dist/diagnostics.d.ts +47 -0
- package/dist/diagnostics.js +8 -0
- package/dist/errors.d.ts +131 -0
- package/dist/errors.js +159 -0
- package/dist/executor.d.ts +66 -0
- package/dist/executor.js +320 -0
- package/dist/expectation.d.ts +61 -0
- package/dist/expectation.js +222 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +36 -0
- package/dist/lexer.d.ts +22 -0
- package/dist/lexer.js +284 -0
- package/dist/loader.d.ts +21 -0
- package/dist/loader.js +81 -0
- package/dist/parser.d.ts +24 -0
- package/dist/parser.js +502 -0
- package/dist/permissions.d.ts +139 -0
- package/dist/permissions.js +325 -0
- package/dist/plugin.d.ts +90 -0
- package/dist/plugin.js +9 -0
- package/dist/plugins/browser.d.ts +24 -0
- package/dist/plugins/browser.js +896 -0
- package/dist/plugins/cli.d.ts +17 -0
- package/dist/plugins/cli.js +134 -0
- package/dist/plugins/db-e2e-probe.d.ts +14 -0
- package/dist/plugins/db-e2e-probe.js +112 -0
- package/dist/plugins/db.d.ts +19 -0
- package/dist/plugins/db.js +199 -0
- package/dist/plugins/demo.d.ts +17 -0
- package/dist/plugins/demo.js +70 -0
- package/dist/plugins/env.d.ts +18 -0
- package/dist/plugins/env.js +87 -0
- package/dist/plugins/fs.d.ts +16 -0
- package/dist/plugins/fs.js +415 -0
- package/dist/plugins/http.d.ts +19 -0
- package/dist/plugins/http.js +505 -0
- package/dist/plugins/jwt.d.ts +17 -0
- package/dist/plugins/jwt.js +342 -0
- package/dist/plugins/sample.d.ts +27 -0
- package/dist/plugins/sample.js +400 -0
- package/dist/plugins/url.d.ts +18 -0
- package/dist/plugins/url.js +126 -0
- package/dist/project-config.d.ts +163 -0
- package/dist/project-config.js +497 -0
- package/dist/registry.d.ts +56 -0
- package/dist/registry.js +110 -0
- package/dist/reporter.d.ts +30 -0
- package/dist/reporter.js +237 -0
- package/dist/run.d.ts +74 -0
- package/dist/run.js +179 -0
- package/dist/runner.d.ts +52 -0
- package/dist/runner.js +38 -0
- package/dist/source.d.ts +37 -0
- package/dist/source.js +31 -0
- package/dist/sources.d.ts +45 -0
- package/dist/sources.js +54 -0
- package/dist/tokens.d.ts +34 -0
- package/dist/tokens.js +25 -0
- package/dist/transport-stdio.d.ts +34 -0
- package/dist/transport-stdio.js +400 -0
- package/dist/values.d.ts +48 -0
- package/dist/values.js +52 -0
- package/dist/workers.d.ts +40 -0
- package/dist/workers.js +26 -0
- package/dist/workspace-none.d.ts +23 -0
- package/dist/workspace-none.js +33 -0
- package/dist/workspace.d.ts +47 -0
- package/dist/workspace.js +116 -0
- package/package.json +28 -0
|
@@ -0,0 +1,342 @@
|
|
|
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 { failure, isFailure, success } from "@sdxc/result";
|
|
12
|
+
import { ToolError } from "../errors.js";
|
|
13
|
+
import { formatValue } from "../values.js";
|
|
14
|
+
/** The only signature algorithm this capability supports; see the module note. */
|
|
15
|
+
const SUPPORTED_ALG = "ES256";
|
|
16
|
+
/** Descriptors of every tool the `jwt` namespace exposes. */
|
|
17
|
+
const DESCRIPTORS = [
|
|
18
|
+
{
|
|
19
|
+
name: "decode",
|
|
20
|
+
summary: "Split a JWT into its header and payload objects, without checking the signature.",
|
|
21
|
+
kind: "observable",
|
|
22
|
+
params: [
|
|
23
|
+
{ name: "token", kind: "value", required: true, summary: "The compact JWT string to read." },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "verify",
|
|
28
|
+
summary: "Verify a JWT's ES256 signature against a JWKS and return its payload.",
|
|
29
|
+
kind: "action",
|
|
30
|
+
requires: "net",
|
|
31
|
+
params: [
|
|
32
|
+
{
|
|
33
|
+
name: "token",
|
|
34
|
+
kind: "value",
|
|
35
|
+
required: true,
|
|
36
|
+
summary: "The compact JWT string to verify.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "jwks_url",
|
|
40
|
+
kind: "value",
|
|
41
|
+
required: true,
|
|
42
|
+
summary: "Absolute URL of the JWKS document that publishes the signing keys.",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Create the built-in `jwt` plugin (namespace `"jwt"`): `jwt.decode` (a
|
|
49
|
+
* permissionless observable) and `jwt.verify` (a `net` action), each
|
|
50
|
+
* returning a {@link ToolError} for a malformed, unverifiable, or expired token.
|
|
51
|
+
*/
|
|
52
|
+
export function createJwtPlugin() {
|
|
53
|
+
return {
|
|
54
|
+
namespace: "jwt",
|
|
55
|
+
describe() {
|
|
56
|
+
return DESCRIPTORS;
|
|
57
|
+
},
|
|
58
|
+
async call(tool, args, context) {
|
|
59
|
+
if (tool === "decode")
|
|
60
|
+
return decode(args);
|
|
61
|
+
if (tool === "verify")
|
|
62
|
+
return await verify(args, context);
|
|
63
|
+
return failure(new ToolError(`jwt has no tool "${tool}"; available tools: decode, verify`));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `jwt.decode <token>` → `{ header, payload }`, each base64url-decoded and
|
|
69
|
+
* JSON-parsed; a pure, permissionless read for asserting on claims without
|
|
70
|
+
* proving the signature.
|
|
71
|
+
*
|
|
72
|
+
* @throws {ToolError} when the token lacks three segments, or a segment is
|
|
73
|
+
* not base64url of a JSON object.
|
|
74
|
+
*/
|
|
75
|
+
function decode(args) {
|
|
76
|
+
let token = readString("decode", args, 0, "token");
|
|
77
|
+
if (isFailure(token))
|
|
78
|
+
return token;
|
|
79
|
+
let split = splitToken("decode", token.data);
|
|
80
|
+
if (isFailure(split))
|
|
81
|
+
return split;
|
|
82
|
+
let header = decodeJsonObject("decode", "header", split.data.header);
|
|
83
|
+
if (isFailure(header))
|
|
84
|
+
return header;
|
|
85
|
+
let payload = decodeJsonObject("decode", "payload", split.data.payload);
|
|
86
|
+
if (isFailure(payload))
|
|
87
|
+
return payload;
|
|
88
|
+
return success({ header: header.data, payload: payload.data });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `jwt.verify <token> <jwks_url>` → the verified payload. The `alg` check
|
|
92
|
+
* runs before any I/O, closing the alg-downgrade path; the `net` permission
|
|
93
|
+
* gate runs before the JWKS fetch, so a denied grant never reaches the network.
|
|
94
|
+
*
|
|
95
|
+
* @throws {ToolError} on a bad `alg`, an unselectable or unimportable key, a
|
|
96
|
+
* failed signature check, or an expired/not-yet-valid token.
|
|
97
|
+
*/
|
|
98
|
+
async function verify(args, context) {
|
|
99
|
+
let token = readString("verify", args, 0, "token");
|
|
100
|
+
if (isFailure(token))
|
|
101
|
+
return token;
|
|
102
|
+
let jwksUrl = readString("verify", args, 1, "jwks_url");
|
|
103
|
+
if (isFailure(jwksUrl))
|
|
104
|
+
return jwksUrl;
|
|
105
|
+
let split = splitToken("verify", token.data);
|
|
106
|
+
if (isFailure(split))
|
|
107
|
+
return split;
|
|
108
|
+
let header = decodeJsonObject("verify", "header", split.data.header);
|
|
109
|
+
if (isFailure(header))
|
|
110
|
+
return header;
|
|
111
|
+
let alg = header.data.alg;
|
|
112
|
+
if (alg !== SUPPORTED_ALG) {
|
|
113
|
+
return failure(new ToolError(`jwt.verify only supports the ${SUPPORTED_ALG} algorithm, but the token header declares ${formatValue(alg ?? null)}`));
|
|
114
|
+
}
|
|
115
|
+
let kid = typeof header.data.kid === "string" ? header.data.kid : undefined;
|
|
116
|
+
let target = parseJwksUrl(jwksUrl.data);
|
|
117
|
+
if (isFailure(target))
|
|
118
|
+
return target;
|
|
119
|
+
let allowed = context.permissions.checkNet(target.data.hostname, portOf(target.data));
|
|
120
|
+
if (isFailure(allowed))
|
|
121
|
+
return allowed;
|
|
122
|
+
let keys = await fetchKeys(target.data);
|
|
123
|
+
if (isFailure(keys))
|
|
124
|
+
return keys;
|
|
125
|
+
let jwk = selectKey(keys.data, kid);
|
|
126
|
+
if (isFailure(jwk))
|
|
127
|
+
return jwk;
|
|
128
|
+
let key = await importKey(jwk.data);
|
|
129
|
+
if (isFailure(key))
|
|
130
|
+
return key;
|
|
131
|
+
let verified = await checkSignature(key.data, `${split.data.header}.${split.data.payload}`, split.data.signature);
|
|
132
|
+
if (isFailure(verified))
|
|
133
|
+
return verified;
|
|
134
|
+
let payload = decodeJsonObject("verify", "payload", split.data.payload);
|
|
135
|
+
if (isFailure(payload))
|
|
136
|
+
return payload;
|
|
137
|
+
let temporal = checkTemporal(payload.data);
|
|
138
|
+
if (isFailure(temporal))
|
|
139
|
+
return temporal;
|
|
140
|
+
return success(payload.data);
|
|
141
|
+
}
|
|
142
|
+
/** Split a compact JWT into its three segments, or a tool error. */
|
|
143
|
+
function splitToken(tool, token) {
|
|
144
|
+
let parts = token.split(".");
|
|
145
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
146
|
+
return failure(new ToolError(`jwt.${tool} expected a compact JWT with three dot-separated segments; got ${parts.length}`));
|
|
147
|
+
}
|
|
148
|
+
return success({ header: parts[0] ?? "", payload: parts[1] ?? "", signature: parts[2] ?? "" });
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* base64url-decode a segment and JSON-parse it into an object. A segment that is
|
|
152
|
+
* not valid base64url, not valid JSON, or JSON that is not an object (a number,
|
|
153
|
+
* string, array, or null) is a tool error naming which segment failed.
|
|
154
|
+
*/
|
|
155
|
+
function decodeJsonObject(tool, part, segment) {
|
|
156
|
+
let bytes = base64urlToBytes(segment);
|
|
157
|
+
if (bytes === null) {
|
|
158
|
+
return failure(new ToolError(`jwt.${tool} could not base64url-decode the ${part} segment`));
|
|
159
|
+
}
|
|
160
|
+
let text;
|
|
161
|
+
try {
|
|
162
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return failure(new ToolError(`jwt.${tool} ${part} segment is not valid UTF-8`));
|
|
166
|
+
}
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(text);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return failure(new ToolError(`jwt.${tool} ${part} segment is not valid JSON`));
|
|
173
|
+
}
|
|
174
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
175
|
+
return failure(new ToolError(`jwt.${tool} ${part} segment is not a JSON object`));
|
|
176
|
+
}
|
|
177
|
+
return success(parsed);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Select the JWKS key a token should verify against: a named `kid` must
|
|
181
|
+
* match exactly, always hard-erroring on a stale or wrong one; an unnamed
|
|
182
|
+
* `kid` falls back to the sole key, or the single EC/P-256 key among several.
|
|
183
|
+
*/
|
|
184
|
+
function selectKey(keys, kid) {
|
|
185
|
+
if (kid !== undefined) {
|
|
186
|
+
let match = keys.find((key) => key.kid === kid);
|
|
187
|
+
if (match === undefined) {
|
|
188
|
+
return failure(new ToolError(`jwt.verify found no JWKS key with kid "${kid}"`));
|
|
189
|
+
}
|
|
190
|
+
return success(match);
|
|
191
|
+
}
|
|
192
|
+
if (keys.length === 1 && keys[0] !== undefined)
|
|
193
|
+
return success(keys[0]);
|
|
194
|
+
let ec = keys.find((key) => key.kty === "EC" && key.crv === "P-256");
|
|
195
|
+
if (ec === undefined) {
|
|
196
|
+
return failure(new ToolError("jwt.verify could not select a signing key: the header has no kid and the JWKS has no unambiguous EC P-256 key"));
|
|
197
|
+
}
|
|
198
|
+
return success(ec);
|
|
199
|
+
}
|
|
200
|
+
/** Import a JWKS entry as an ECDSA P-256 public key restricted to verifying. */
|
|
201
|
+
async function importKey(jwk) {
|
|
202
|
+
let material = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
203
|
+
if (material.kty !== "EC" || material.crv !== "P-256") {
|
|
204
|
+
return failure(new ToolError(`jwt.verify selected a key that is not EC P-256 (kty ${formatValue(material.kty ?? null)}, crv ${formatValue(material.crv ?? null)})`));
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
let key = await crypto.subtle.importKey("jwk", material, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
208
|
+
return success(key);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return failure(new ToolError(`jwt.verify could not import the signing key: ${describeError(error)}`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Verify the ES256 signature over the signing input `header.payload`. WebCrypto
|
|
216
|
+
* consumes the raw 64-byte r‖s signature that JWS carries directly; a decode
|
|
217
|
+
* failure or a false result is a signature tool error.
|
|
218
|
+
*/
|
|
219
|
+
async function checkSignature(key, signingInput, signatureSegment) {
|
|
220
|
+
let signature = base64urlToBytes(signatureSegment);
|
|
221
|
+
if (signature === null) {
|
|
222
|
+
return failure(new ToolError("jwt.verify could not base64url-decode the signature segment"));
|
|
223
|
+
}
|
|
224
|
+
let ok;
|
|
225
|
+
try {
|
|
226
|
+
ok = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, signature, new TextEncoder().encode(signingInput));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return failure(new ToolError(`jwt.verify could not check the signature: ${describeError(error)}`));
|
|
230
|
+
}
|
|
231
|
+
if (!ok)
|
|
232
|
+
return failure(new ToolError("jwt.verify signature check failed: the token is not signed by the JWKS key"));
|
|
233
|
+
return success(undefined);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Reject an expired or not-yet-valid token: `exp` is required and, at or
|
|
237
|
+
* before now, expired; a present `nbf` in the future is not yet valid. Both
|
|
238
|
+
* compare with zero clock skew, for deterministic specs.
|
|
239
|
+
*/
|
|
240
|
+
function checkTemporal(payload) {
|
|
241
|
+
let now = Math.floor(Date.now() / 1000);
|
|
242
|
+
let exp = payload.exp;
|
|
243
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) {
|
|
244
|
+
return failure(new ToolError("jwt.verify requires a numeric exp claim, but the token has none"));
|
|
245
|
+
}
|
|
246
|
+
if (exp <= now) {
|
|
247
|
+
return failure(new ToolError(`jwt.verify token expired: exp ${exp} is at or before now ${now}`));
|
|
248
|
+
}
|
|
249
|
+
let nbf = payload.nbf;
|
|
250
|
+
if (typeof nbf === "number" && Number.isFinite(nbf) && nbf > now) {
|
|
251
|
+
return failure(new ToolError(`jwt.verify token not yet valid: nbf ${nbf} is after now ${now}`));
|
|
252
|
+
}
|
|
253
|
+
return success(undefined);
|
|
254
|
+
}
|
|
255
|
+
/** Fetch and validate a JWKS document, returning its `keys` array. */
|
|
256
|
+
async function fetchKeys(url) {
|
|
257
|
+
let response;
|
|
258
|
+
try {
|
|
259
|
+
response = await fetch(url);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return failure(new ToolError(`jwt.verify could not fetch the JWKS at ${url.href}: ${describeError(error)}`));
|
|
263
|
+
}
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
return failure(new ToolError(`jwt.verify got HTTP ${response.status} fetching the JWKS at ${url.href}`));
|
|
266
|
+
}
|
|
267
|
+
let body;
|
|
268
|
+
try {
|
|
269
|
+
body = await response.json();
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return failure(new ToolError(`jwt.verify got a non-JSON JWKS body from ${url.href}`));
|
|
273
|
+
}
|
|
274
|
+
if (typeof body !== "object" ||
|
|
275
|
+
body === null ||
|
|
276
|
+
!Array.isArray(body.keys)) {
|
|
277
|
+
return failure(new ToolError(`jwt.verify JWKS at ${url.href} has no "keys" array`));
|
|
278
|
+
}
|
|
279
|
+
let keys = [];
|
|
280
|
+
for (let key of body.keys) {
|
|
281
|
+
if (typeof key === "object" && key !== null && !Array.isArray(key))
|
|
282
|
+
keys.push(key);
|
|
283
|
+
}
|
|
284
|
+
if (keys.length === 0) {
|
|
285
|
+
return failure(new ToolError(`jwt.verify JWKS at ${url.href} contains no usable keys`));
|
|
286
|
+
}
|
|
287
|
+
return success(keys);
|
|
288
|
+
}
|
|
289
|
+
/** Parse the JWKS URL, requiring an absolute http(s) URL. */
|
|
290
|
+
function parseJwksUrl(raw) {
|
|
291
|
+
let url;
|
|
292
|
+
try {
|
|
293
|
+
url = new URL(raw);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return failure(new ToolError(`jwt.verify requires an absolute JWKS URL; got "${raw}"`));
|
|
297
|
+
}
|
|
298
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
299
|
+
return failure(new ToolError(`jwt.verify supports absolute http(s) JWKS URLs only; got "${raw}"`));
|
|
300
|
+
}
|
|
301
|
+
return success(url);
|
|
302
|
+
}
|
|
303
|
+
/** The port the JWKS fetch will reach: the URL's own, or the scheme default. */
|
|
304
|
+
function portOf(url) {
|
|
305
|
+
if (url.port !== "")
|
|
306
|
+
return Number(url.port);
|
|
307
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Decode an unpadded base64url segment into bytes, tolerating standard-alphabet
|
|
311
|
+
* input too. Returns null when the segment is not valid base64. The buffer is a
|
|
312
|
+
* plain `ArrayBuffer` so the bytes satisfy WebCrypto's `BufferSource` parameter.
|
|
313
|
+
*/
|
|
314
|
+
function base64urlToBytes(segment) {
|
|
315
|
+
let normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
316
|
+
let pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
|
|
317
|
+
let binary;
|
|
318
|
+
try {
|
|
319
|
+
binary = atob(normalized + pad);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
let bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
325
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
326
|
+
bytes[index] = binary.charCodeAt(index);
|
|
327
|
+
return bytes;
|
|
328
|
+
}
|
|
329
|
+
/** Read the argument at `index` as a required string, or a tool error. */
|
|
330
|
+
function readString(tool, args, index, name) {
|
|
331
|
+
let arg = args[index];
|
|
332
|
+
if (arg === undefined || arg.kind !== "value" || typeof arg.value !== "string") {
|
|
333
|
+
return failure(new ToolError(`jwt.${tool} requires its ${name} argument to be a string`));
|
|
334
|
+
}
|
|
335
|
+
return success(arg.value);
|
|
336
|
+
}
|
|
337
|
+
/** Render an unknown thrown value as a one-line message. */
|
|
338
|
+
function describeError(error) {
|
|
339
|
+
if (error instanceof Error)
|
|
340
|
+
return error.message;
|
|
341
|
+
return String(error);
|
|
342
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `sample` capability: generated input for a spec that needs a
|
|
3
|
+
* name, an address, or fifty of something, without a literal typed into the
|
|
4
|
+
* suite. Every tool draws from the test's own stream, so the same test sees
|
|
5
|
+
* the same values on every run whatever else the suite is doing.
|
|
6
|
+
*
|
|
7
|
+
* A call target carries at most one dot, so a module reaches a spec as one
|
|
8
|
+
* zero-argument tool returning every field that module generates —
|
|
9
|
+
* `let who = sample.person` then `who.job_title`. Generators that need an
|
|
10
|
+
* argument keep a tool of their own. Fields are named the way a spec names
|
|
11
|
+
* things, in snake case.
|
|
12
|
+
*
|
|
13
|
+
* The tools are actions rather than observations: a draw advances the stream,
|
|
14
|
+
* and polling `eventually` until a random value matches is never what an
|
|
15
|
+
* author meant. None needs a permission — generation is computation, reaching
|
|
16
|
+
* nothing outside the process.
|
|
17
|
+
*
|
|
18
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
19
|
+
* @copyright Sergio Xalambrí 2026
|
|
20
|
+
*/
|
|
21
|
+
import type { Plugin } from "../plugin.js";
|
|
22
|
+
/**
|
|
23
|
+
* Create the built-in `sample` plugin (namespace `"sample"`). Values come from
|
|
24
|
+
* the stream on the call's context, so two runs of one test generate the same
|
|
25
|
+
* data and two tests never draw from each other's stream.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSamplePlugin(): Plugin;
|