@fedify/cli 2.3.0-dev.994 → 2.4.0-dev.1417

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 (83) hide show
  1. package/dist/bench/action.js +469 -0
  2. package/dist/bench/actor/documents.js +39 -0
  3. package/dist/bench/actor/fleet.js +39 -0
  4. package/dist/bench/actor/keys.js +35 -0
  5. package/dist/bench/command.js +72 -0
  6. package/dist/bench/compare/schema.js +16 -0
  7. package/dist/bench/compare.js +667 -0
  8. package/dist/bench/discovery/discover.js +67 -0
  9. package/dist/bench/discovery/probe.js +50 -0
  10. package/dist/bench/load/arrival.js +27 -0
  11. package/dist/bench/load/clock.js +33 -0
  12. package/dist/bench/load/generator.js +145 -0
  13. package/dist/bench/metrics/aggregate.js +64 -0
  14. package/dist/bench/metrics/histogram.js +141 -0
  15. package/dist/bench/metrics/stats-client.js +216 -0
  16. package/dist/bench/mod.js +11 -0
  17. package/dist/bench/render/format.js +46 -0
  18. package/dist/bench/render/index.js +20 -0
  19. package/dist/bench/render/json.js +12 -0
  20. package/dist/bench/render/markdown.js +63 -0
  21. package/dist/bench/render/text.js +75 -0
  22. package/dist/bench/result/build.js +252 -0
  23. package/dist/bench/result/expect/assert.js +74 -0
  24. package/dist/bench/result/expect/evaluate.js +128 -0
  25. package/dist/bench/result/expect/metrics.js +34 -0
  26. package/dist/bench/result/schema.js +365 -0
  27. package/dist/bench/safety/gate.js +62 -0
  28. package/dist/bench/safety/tiers.js +97 -0
  29. package/dist/bench/scenario/coerce.js +24 -0
  30. package/dist/bench/scenario/errors.js +36 -0
  31. package/dist/bench/scenario/load.js +69 -0
  32. package/dist/bench/scenario/normalize.js +125 -0
  33. package/dist/bench/scenario/schema.js +399 -0
  34. package/dist/bench/scenario/units.js +56 -0
  35. package/dist/bench/scenario/validate.js +29 -0
  36. package/dist/bench/scenarios/actor.js +38 -0
  37. package/dist/bench/scenarios/failure.js +363 -0
  38. package/dist/bench/scenarios/fanout.js +261 -0
  39. package/dist/bench/scenarios/inbox.js +147 -0
  40. package/dist/bench/scenarios/mixed.js +244 -0
  41. package/dist/bench/scenarios/object-discovery.js +211 -0
  42. package/dist/bench/scenarios/object.js +54 -0
  43. package/dist/bench/scenarios/read.js +108 -0
  44. package/dist/bench/scenarios/registry.js +39 -0
  45. package/dist/bench/scenarios/runner.js +96 -0
  46. package/dist/bench/scenarios/webfinger.js +44 -0
  47. package/dist/bench/server/synthetic.js +118 -0
  48. package/dist/bench/signing/activity-id.js +18 -0
  49. package/dist/bench/signing/pipeline.js +134 -0
  50. package/dist/bench/signing/signer.js +39 -0
  51. package/dist/bench/template/generate.js +90 -0
  52. package/dist/bench/template/helpers.js +19 -0
  53. package/dist/bench/template/template.js +132 -0
  54. package/dist/cache.js +2 -2
  55. package/dist/commands.js +110 -0
  56. package/dist/config.js +15 -3
  57. package/dist/deno.js +1 -1
  58. package/dist/docloader.js +1 -1
  59. package/dist/generate-vocab/action.js +3 -3
  60. package/dist/generate-vocab/command.js +6 -4
  61. package/dist/imagerenderer.js +3 -3
  62. package/dist/inbox/command.js +6 -4
  63. package/dist/inbox/view.js +1 -1
  64. package/dist/inbox.js +4 -4
  65. package/dist/log.js +2 -2
  66. package/dist/lookup/command.js +121 -0
  67. package/dist/lookup.js +27 -138
  68. package/dist/mod.js +2 -20
  69. package/dist/nodeinfo.js +53 -12
  70. package/dist/options.js +1 -1
  71. package/dist/relay/command.js +6 -4
  72. package/dist/relay.js +3 -3
  73. package/dist/runner.js +70 -45
  74. package/dist/tunnel.js +8 -6
  75. package/dist/utils.js +9 -4
  76. package/dist/webfinger/action.js +1 -1
  77. package/dist/webfinger/command.js +6 -4
  78. package/dist/webfinger/error.js +2 -0
  79. package/dist/webfinger/lib.js +1 -1
  80. package/package.json +28 -23
  81. package/dist/generate-vocab/mod.js +0 -4
  82. package/dist/init/mod.js +0 -3
  83. package/dist/webfinger/mod.js +0 -4
@@ -0,0 +1,365 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/result/schema.ts
3
+ /**
4
+ * The embedded JSON Schema (draft 2020-12) for benchmark report output.
5
+ *
6
+ * Like the scenario schema, this object is the runtime copy and is published,
7
+ * byte-for-byte, as files under *schema/bench/*; a drift guard keeps the two in
8
+ * sync. The matching TypeScript types live in {@link ./model.ts}.
9
+ * @since 2.3.0
10
+ * @module
11
+ */
12
+ /** The hosted URL that serves the report schema. */
13
+ const REPORT_SCHEMA_ID = "https://json-schema.fedify.dev/bench/report-v3.json";
14
+ /** The hosted URL for the version 2 report schema. */
15
+ const REPORT_SCHEMA_V2_ID = "https://json-schema.fedify.dev/bench/report-v2.json";
16
+ /** The benchmark report JSON Schema (draft 2020-12). */
17
+ const reportSchemaV1 = {
18
+ $schema: "https://json-schema.org/draft/2020-12/schema",
19
+ $id: "https://json-schema.fedify.dev/bench/report-v1.json",
20
+ title: "Fedify benchmark report",
21
+ type: "object",
22
+ additionalProperties: false,
23
+ required: [
24
+ "schemaVersion",
25
+ "tool",
26
+ "environment",
27
+ "target",
28
+ "startedAt",
29
+ "finishedAt",
30
+ "suite",
31
+ "passed",
32
+ "scenarios"
33
+ ],
34
+ properties: {
35
+ $schema: { type: "string" },
36
+ schemaVersion: { const: 1 },
37
+ tool: {
38
+ type: "object",
39
+ additionalProperties: false,
40
+ required: ["name", "version"],
41
+ properties: {
42
+ name: { type: "string" },
43
+ version: { type: "string" }
44
+ }
45
+ },
46
+ environment: {
47
+ type: "object",
48
+ additionalProperties: false,
49
+ required: [
50
+ "runtime",
51
+ "runtimeVersion",
52
+ "os",
53
+ "cpuCount"
54
+ ],
55
+ properties: {
56
+ runtime: { type: "string" },
57
+ runtimeVersion: { type: "string" },
58
+ os: { type: "string" },
59
+ cpuCount: {
60
+ type: "integer",
61
+ minimum: 0
62
+ }
63
+ }
64
+ },
65
+ target: {
66
+ type: "object",
67
+ additionalProperties: false,
68
+ required: ["url", "statsAvailable"],
69
+ properties: {
70
+ url: { type: "string" },
71
+ fedifyVersion: { type: ["string", "null"] },
72
+ statsAvailable: { type: "boolean" }
73
+ }
74
+ },
75
+ startedAt: { type: "string" },
76
+ finishedAt: { type: "string" },
77
+ suite: {
78
+ type: "object",
79
+ additionalProperties: false,
80
+ required: ["configHash"],
81
+ properties: {
82
+ name: { type: "string" },
83
+ configHash: { type: "string" }
84
+ }
85
+ },
86
+ passed: { type: "boolean" },
87
+ scenarios: {
88
+ type: "array",
89
+ items: { $ref: "#/$defs/scenarioResult" }
90
+ }
91
+ },
92
+ $defs: {
93
+ latencyMs: {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ required: [
97
+ "p50",
98
+ "p95",
99
+ "p99",
100
+ "mean",
101
+ "max"
102
+ ],
103
+ properties: {
104
+ p50: { type: "number" },
105
+ p95: { type: "number" },
106
+ p99: { type: "number" },
107
+ mean: { type: "number" },
108
+ max: { type: "number" }
109
+ }
110
+ },
111
+ partialLatencyMs: {
112
+ type: "object",
113
+ additionalProperties: false,
114
+ properties: {
115
+ p50: { type: "number" },
116
+ p95: { type: "number" },
117
+ p99: { type: "number" }
118
+ }
119
+ },
120
+ loadSummary: {
121
+ type: "object",
122
+ additionalProperties: false,
123
+ required: [
124
+ "model",
125
+ "durationMs",
126
+ "warmupMs"
127
+ ],
128
+ properties: {
129
+ model: { enum: ["open", "closed"] },
130
+ ratePerSec: { type: "number" },
131
+ arrival: { type: "string" },
132
+ concurrency: { type: "integer" },
133
+ durationMs: { type: "number" },
134
+ warmupMs: { type: "number" },
135
+ maxInFlight: { type: "integer" }
136
+ },
137
+ oneOf: [{
138
+ properties: { model: { const: "open" } },
139
+ required: ["ratePerSec", "arrival"],
140
+ not: { required: ["concurrency"] }
141
+ }, {
142
+ properties: { model: { const: "closed" } },
143
+ required: ["concurrency"],
144
+ not: { anyOf: [{ required: ["ratePerSec"] }, { required: ["arrival"] }] }
145
+ }]
146
+ },
147
+ requestSummary: {
148
+ type: "object",
149
+ additionalProperties: false,
150
+ required: [
151
+ "total",
152
+ "ok",
153
+ "failed",
154
+ "successRate"
155
+ ],
156
+ properties: {
157
+ total: {
158
+ type: "integer",
159
+ minimum: 0
160
+ },
161
+ ok: {
162
+ type: "integer",
163
+ minimum: 0
164
+ },
165
+ failed: {
166
+ type: "integer",
167
+ minimum: 0
168
+ },
169
+ successRate: {
170
+ type: "number",
171
+ minimum: 0,
172
+ maximum: 1
173
+ }
174
+ }
175
+ },
176
+ clientMetrics: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ required: ["latencyMs"],
180
+ properties: { latencyMs: { $ref: "#/$defs/latencyMs" } }
181
+ },
182
+ serverMetrics: {
183
+ type: "object",
184
+ additionalProperties: false,
185
+ properties: {
186
+ signatureVerificationMs: {
187
+ type: "object",
188
+ additionalProperties: false,
189
+ required: ["overall"],
190
+ properties: {
191
+ overall: { $ref: "#/$defs/partialLatencyMs" },
192
+ byStandard: {
193
+ type: "object",
194
+ additionalProperties: { $ref: "#/$defs/partialLatencyMs" }
195
+ }
196
+ }
197
+ },
198
+ queue: {
199
+ type: "object",
200
+ additionalProperties: false,
201
+ properties: {
202
+ drainMs: { $ref: "#/$defs/partialLatencyMs" },
203
+ depthMax: { type: "number" }
204
+ }
205
+ }
206
+ }
207
+ },
208
+ errorBucket: {
209
+ type: "object",
210
+ additionalProperties: false,
211
+ required: [
212
+ "kind",
213
+ "reason",
214
+ "count"
215
+ ],
216
+ properties: {
217
+ kind: { type: "string" },
218
+ status: { type: "integer" },
219
+ reason: { type: "string" },
220
+ count: {
221
+ type: "integer",
222
+ minimum: 0
223
+ }
224
+ }
225
+ },
226
+ expectResult: {
227
+ type: "object",
228
+ additionalProperties: false,
229
+ required: [
230
+ "metric",
231
+ "op",
232
+ "threshold",
233
+ "unit",
234
+ "actual",
235
+ "severity",
236
+ "pass"
237
+ ],
238
+ properties: {
239
+ metric: { type: "string" },
240
+ op: { enum: [
241
+ "lt",
242
+ "lte",
243
+ "gt",
244
+ "gte",
245
+ "eq"
246
+ ] },
247
+ threshold: { type: "number" },
248
+ unit: { type: ["string", "null"] },
249
+ actual: { type: ["number", "null"] },
250
+ severity: { enum: ["warn", "fail"] },
251
+ pass: { type: "boolean" }
252
+ }
253
+ },
254
+ scenarioResult: {
255
+ type: "object",
256
+ additionalProperties: false,
257
+ required: [
258
+ "name",
259
+ "type",
260
+ "load",
261
+ "requests",
262
+ "throughputPerSec",
263
+ "client",
264
+ "server",
265
+ "errors",
266
+ "expectations",
267
+ "passed"
268
+ ],
269
+ properties: {
270
+ name: { type: "string" },
271
+ type: { enum: [
272
+ "inbox",
273
+ "webfinger",
274
+ "actor",
275
+ "object",
276
+ "fanout",
277
+ "collection",
278
+ "failure",
279
+ "mixed"
280
+ ] },
281
+ load: { $ref: "#/$defs/loadSummary" },
282
+ requests: { $ref: "#/$defs/requestSummary" },
283
+ throughputPerSec: { type: "number" },
284
+ client: { $ref: "#/$defs/clientMetrics" },
285
+ server: { anyOf: [{ $ref: "#/$defs/serverMetrics" }, { type: "null" }] },
286
+ errors: {
287
+ type: "array",
288
+ items: { $ref: "#/$defs/errorBucket" }
289
+ },
290
+ expectations: {
291
+ type: "array",
292
+ items: { $ref: "#/$defs/expectResult" }
293
+ },
294
+ passed: { type: "boolean" },
295
+ histogram: { $ref: "#/$defs/serializedHistogram" }
296
+ }
297
+ },
298
+ serializedHistogram: {
299
+ type: "object",
300
+ additionalProperties: false,
301
+ required: [
302
+ "version",
303
+ "subBucketCount",
304
+ "count",
305
+ "zeroCount",
306
+ "min",
307
+ "max",
308
+ "sum",
309
+ "indices",
310
+ "counts"
311
+ ],
312
+ properties: {
313
+ version: { const: 1 },
314
+ subBucketCount: {
315
+ type: "integer",
316
+ minimum: 1
317
+ },
318
+ count: {
319
+ type: "integer",
320
+ minimum: 0
321
+ },
322
+ zeroCount: {
323
+ type: "integer",
324
+ minimum: 0
325
+ },
326
+ min: { type: "number" },
327
+ max: { type: "number" },
328
+ sum: { type: "number" },
329
+ indices: {
330
+ type: "array",
331
+ items: { type: "integer" }
332
+ },
333
+ counts: {
334
+ type: "array",
335
+ items: {
336
+ type: "integer",
337
+ minimum: 0
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ };
344
+ /** The benchmark report JSON Schema (draft 2020-12). */
345
+ const reportSchemaV2 = {
346
+ ...reportSchemaV1,
347
+ $id: REPORT_SCHEMA_V2_ID,
348
+ properties: {
349
+ ...reportSchemaV1.properties,
350
+ schemaVersion: { const: 2 }
351
+ },
352
+ $defs: {
353
+ ...reportSchemaV1.$defs,
354
+ scenarioResult: {
355
+ ...reportSchemaV1.$defs.scenarioResult,
356
+ properties: {
357
+ ...reportSchemaV1.$defs.scenarioResult.properties,
358
+ deliveryThroughputPerSec: { type: "number" }
359
+ }
360
+ }
361
+ }
362
+ };
363
+ ({ ...reportSchemaV2 }), { ...reportSchemaV2.properties }, { ...reportSchemaV2.$defs }, { ...reportSchemaV2.$defs.scenarioResult }, [...reportSchemaV2.$defs.scenarioResult.required], { ...reportSchemaV2.$defs.scenarioResult.properties };
364
+ //#endregion
365
+ export { REPORT_SCHEMA_ID };
@@ -0,0 +1,62 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/safety/gate.ts
3
+ /** An error raised when a target is refused by the safety gate. */
4
+ var UnsafeTargetError = class extends Error {};
5
+ /**
6
+ * Asserts that a target may be benchmarked, throwing otherwise.
7
+ * @param context The gate decision inputs.
8
+ * @throws {UnsafeTargetError} If the target is public, does not advertise
9
+ * benchmark mode, and `--allow-unsafe-target` was not given.
10
+ */
11
+ function assertTargetAllowed(context) {
12
+ if (context.dryRun) return;
13
+ if (context.tier !== "public") return;
14
+ if (context.benchmarkMode) return;
15
+ if (context.allowUnsafe) return;
16
+ throw new UnsafeTargetError("Refusing to benchmark a public target that does not advertise benchmark mode. If you control this target, pass --allow-unsafe-target (mandatory in CI and any non-interactive context).");
17
+ }
18
+ /**
19
+ * Asserts that an unsafe public-target override is specific and bounded.
20
+ *
21
+ * The override is only meaningful for a public target that does not advertise
22
+ * benchmark mode. In that caution tier, the operator must name the target on
23
+ * the command line for this run and must explicitly set load, duration, and
24
+ * runs, so the built-in defaults cannot accidentally create a long public
25
+ * benchmark.
26
+ * @param context The unsafe override decision inputs.
27
+ * @throws {UnsafeTargetError} If the unsafe override is too broad.
28
+ */
29
+ function assertUnsafeOverrideAllowed(context) {
30
+ if (context.tier !== "public" || context.benchmarkMode || !context.allowUnsafe) return;
31
+ if (!context.explicitCliTarget) throw new UnsafeTargetError("The --allow-unsafe-target override must be paired with an explicit --target for this run.");
32
+ for (const scenario of context.scenarios) {
33
+ if (!scenario.explicitLoad) throw new UnsafeTargetError(`Scenario "${scenario.name}" uses the built-in benchmark load default. Set rate or concurrency explicitly before using --allow-unsafe-target against a public target.`);
34
+ if (!scenario.explicitDuration) throw new UnsafeTargetError(`Scenario "${scenario.name}" uses the built-in benchmark duration default. Set duration explicitly before using --allow-unsafe-target against a public target.`);
35
+ if (!scenario.explicitRuns) throw new UnsafeTargetError(`Scenario "${scenario.name}" uses the built-in benchmark runs default. Set runs explicitly before using --allow-unsafe-target against a public target.`);
36
+ }
37
+ }
38
+ /**
39
+ * Asserts that a resolved inbox URL — the actual destination of signed
40
+ * benchmark load — may be sent to. The suite's `target` is gated separately by
41
+ * {@link assertTargetAllowed}; this catches a destination that differs from it
42
+ * (a public `recipient`, or an explicit `inbox:` URL), so production cannot be
43
+ * benchmarked through the back door.
44
+ *
45
+ * A destination is allowed when it is loopback or private, or shares the gated
46
+ * target's host while the target advertises benchmark mode (inheriting its
47
+ * gate), or `--allow-unsafe-target` is given. Because the destination's server
48
+ * dereferences the synthetic actor while verifying signatures, a non-loopback
49
+ * destination additionally requires an advertised, reachable synthetic host.
50
+ * @param url The resolved inbox URL.
51
+ * @param context The destination gate inputs.
52
+ * @throws {UnsafeTargetError} If the destination is refused.
53
+ */
54
+ function assertInboxDestinationAllowed(url, context) {
55
+ const sameOrigin = url.origin === context.targetOrigin;
56
+ const tier = sameOrigin ? context.targetTier : context.destinationTier;
57
+ const inheritsTargetGate = sameOrigin && context.targetBenchmarkMode;
58
+ if (tier === "public" && !inheritsTargetGate && !context.allowUnsafe) throw new UnsafeTargetError(`Refusing to send benchmark load to ${url.href}: it is a public inbox that is neither part of the benchmarked target nor covered by benchmark mode. Pass --allow-unsafe-target to override.`);
59
+ if (tier !== "loopback" && !context.advertised) throw new UnsafeTargetError(`Refusing to send signed benchmark load to ${url.href}: the synthetic actor server is unreachable from a non-loopback inbox. Pass --advertise-host with an address it can reach.`);
60
+ }
61
+ //#endregion
62
+ export { UnsafeTargetError, assertInboxDestinationAllowed, assertTargetAllowed, assertUnsafeOverrideAllowed };
@@ -0,0 +1,97 @@
1
+ import "@js-temporal/polyfill";
2
+ import { lookup } from "node:dns/promises";
3
+ //#region src/bench/safety/tiers.ts
4
+ /**
5
+ * Target risk classification.
6
+ *
7
+ * A target is `loopback` or `private` when it is clearly one of the operator's
8
+ * own boxes, and `public` otherwise. Classification is conservative: a host
9
+ * that is not obviously loopback or private is treated as `public` (the gated
10
+ * tier), since the tool cannot tell staging from production without resolving
11
+ * and trusting DNS.
12
+ * @since 2.3.0
13
+ * @module
14
+ */
15
+ /**
16
+ * Classifies a target URL into a risk tier from its host.
17
+ * @param target The target URL.
18
+ * @returns The risk tier.
19
+ */
20
+ function classifyTarget(target) {
21
+ let host = target.hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
22
+ if (host.endsWith(".")) host = host.slice(0, -1);
23
+ if (host === "localhost" || host.endsWith(".localhost")) return "loopback";
24
+ if (host.endsWith(".local")) return "private";
25
+ if (isIpv4(host)) return classifyIpv4(host);
26
+ if (host.includes(":")) return classifyIpv6(host);
27
+ return "public";
28
+ }
29
+ /**
30
+ * Classifies a target URL, resolving DNS for public-looking hostnames.
31
+ *
32
+ * Literal addresses and known local hostname suffixes are classified directly.
33
+ * Other hostnames are resolved and classified from their addresses; any public
34
+ * address in the answer keeps the target public, and DNS failure is treated as
35
+ * public so the safety gate remains conservative.
36
+ * @param target The target URL.
37
+ * @param resolveAddresses Hostname resolver, overridable for tests.
38
+ * @returns The resolved target tier.
39
+ */
40
+ async function classifyResolvedTarget(target, resolveAddresses = defaultResolveTargetAddresses) {
41
+ const host = normalizedHost(target);
42
+ const direct = classifyTarget(target);
43
+ if (direct !== "public" || isIpLiteral(host)) return direct;
44
+ try {
45
+ const resolved = await resolveAddresses(host);
46
+ if (!Array.isArray(resolved) || resolved.length < 1) return "public";
47
+ let aggregate = "loopback";
48
+ for (const address of resolved) {
49
+ const tier = classifyTarget(new URL(`http://${hostForAddress(address)}/`));
50
+ if (tier === "public") return "public";
51
+ if (tier === "private") aggregate = "private";
52
+ }
53
+ return aggregate;
54
+ } catch {
55
+ return "public";
56
+ }
57
+ }
58
+ /** Resolves a hostname with the platform DNS resolver. */
59
+ async function defaultResolveTargetAddresses(hostname) {
60
+ return (await lookup(hostname, { all: true })).map((entry) => entry.address);
61
+ }
62
+ function normalizedHost(target) {
63
+ let host = target.hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
64
+ if (host.endsWith(".")) host = host.slice(0, -1);
65
+ return host;
66
+ }
67
+ function isIpLiteral(host) {
68
+ return isIpv4(host) || host.includes(":");
69
+ }
70
+ function hostForAddress(address) {
71
+ return address.includes(":") ? `[${address}]` : address;
72
+ }
73
+ function isIpv4(host) {
74
+ const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
75
+ return match != null && match.slice(1).every((octet) => Number(octet) <= 255);
76
+ }
77
+ function classifyIpv4(host) {
78
+ if (host === "0.0.0.0" || /^127\./.test(host)) return "loopback";
79
+ if (/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host)) return "private";
80
+ return "public";
81
+ }
82
+ function classifyIpv6(host) {
83
+ if (host === "::1") return "loopback";
84
+ const dotted = host.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
85
+ if (dotted != null && isIpv4(dotted[1])) return classifyIpv4(dotted[1]);
86
+ const hex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
87
+ if (hex != null) {
88
+ const hi = Number.parseInt(hex[1], 16);
89
+ const lo = Number.parseInt(hex[2], 16);
90
+ return classifyIpv4(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
91
+ }
92
+ if (/^f[cd][0-9a-f]*:/.test(host)) return "private";
93
+ if (/^fe[89ab][0-9a-f]*:/.test(host)) return "private";
94
+ return "public";
95
+ }
96
+ //#endregion
97
+ export { classifyResolvedTarget };
@@ -0,0 +1,24 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/scenario/coerce.ts
3
+ /**
4
+ * Scalar-or-list coercion used throughout the scenario format, where many
5
+ * fields (`recipient`, `seed`, `collection`, `type`, and so on) accept either a
6
+ * single value or a list of values so the common single-value case stays terse.
7
+ * @since 2.3.0
8
+ * @module
9
+ */
10
+ /**
11
+ * Normalizes a scalar-or-list value into an array.
12
+ *
13
+ * A single value becomes a one-element array, an array is shallow-copied, and
14
+ * `null`/`undefined` becomes an empty array.
15
+ * @typeParam T The element type.
16
+ * @param value A single value, a list of values, or nothing.
17
+ * @returns A new array of values.
18
+ */
19
+ function asList(value) {
20
+ if (value == null) return [];
21
+ return Array.isArray(value) ? [...value] : [value];
22
+ }
23
+ //#endregion
24
+ export { asList };
@@ -0,0 +1,36 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/scenario/errors.ts
3
+ /** An error raised when a scenario suite fails schema validation. */
4
+ var SuiteValidationError = class extends Error {
5
+ /** The individual validation problems, most specific first. */
6
+ problems;
7
+ constructor(problems, source) {
8
+ super(formatMessage(problems, source));
9
+ this.name = "SuiteValidationError";
10
+ this.problems = problems;
11
+ }
12
+ };
13
+ function formatMessage(problems, source) {
14
+ const where = source == null ? "scenario suite" : source;
15
+ if (problems.length === 0) return `Invalid ${where}.`;
16
+ return `Invalid ${where}:\n${dedupe(problems).map((problem) => {
17
+ return ` - ${problem.instanceLocation === "#" || problem.instanceLocation === "" ? "(root)" : problem.instanceLocation.replace(/^#/, "")}: ${problem.error}`;
18
+ }).join("\n")}`;
19
+ }
20
+ function dedupe(problems) {
21
+ const seen = /* @__PURE__ */ new Set();
22
+ const result = [];
23
+ const sorted = [...problems].sort((a, b) => depth(b.instanceLocation) - depth(a.instanceLocation));
24
+ for (const problem of sorted) {
25
+ const key = JSON.stringify([problem.instanceLocation, problem.error]);
26
+ if (seen.has(key)) continue;
27
+ seen.add(key);
28
+ result.push(problem);
29
+ }
30
+ return result;
31
+ }
32
+ function depth(instanceLocation) {
33
+ return (instanceLocation.match(/\//g) ?? []).length;
34
+ }
35
+ //#endregion
36
+ export { SuiteValidationError };
@@ -0,0 +1,69 @@
1
+ import "@js-temporal/polyfill";
2
+ import { defaultHelpers } from "../template/helpers.js";
3
+ import { renderTemplates } from "../template/template.js";
4
+ import { readFile } from "node:fs/promises";
5
+ import { parse } from "yaml";
6
+ //#region src/bench/scenario/load.ts
7
+ /**
8
+ * Reading and parsing scenario suite files.
9
+ *
10
+ * Files may be written in YAML or JSON; because YAML is a superset of JSON, a
11
+ * single YAML parser handles both, and YAML anchors/aliases are available for
12
+ * in-document reuse.
13
+ * @since 2.3.0
14
+ * @module
15
+ */
16
+ /**
17
+ * Parses scenario suite text (YAML or JSON) into an untyped value.
18
+ * @param text The file contents.
19
+ * @returns The parsed value, to be validated with `validateSuite()`.
20
+ */
21
+ function parseSuiteText(text) {
22
+ return parse(text);
23
+ }
24
+ /**
25
+ * Reads and parses a scenario suite file.
26
+ * @param path The path to the suite file.
27
+ * @returns The parsed value, to be validated with `validateSuite()`.
28
+ */
29
+ async function loadSuiteFile(path) {
30
+ return parseSuiteText(await readFile(path, { encoding: "utf-8" }));
31
+ }
32
+ /**
33
+ * Expands `${{ ... }}` templates in a parsed suite.
34
+ *
35
+ * The context exposes `target` (its `host`, `hostname`, `port`, `origin`,
36
+ * `href`, and `protocol`) plus the default helpers. The target comes from the
37
+ * `--target` override or the suite's own `target`, neither of which is
38
+ * templated.
39
+ * @param raw The parsed suite value.
40
+ * @param targetOverride A target URL from `--target`, if any.
41
+ * @returns The suite with templates expanded.
42
+ */
43
+ function renderSuiteTemplates(raw, targetOverride) {
44
+ const target = targetOverride ?? suiteTarget(raw);
45
+ const values = {};
46
+ if (target != null) try {
47
+ const url = new URL(target);
48
+ values.target = {
49
+ host: url.host,
50
+ hostname: url.hostname,
51
+ port: url.port,
52
+ origin: url.origin,
53
+ href: url.href,
54
+ protocol: url.protocol.replace(/:$/, "")
55
+ };
56
+ } catch {}
57
+ return renderTemplates(raw, {
58
+ values,
59
+ helpers: defaultHelpers()
60
+ });
61
+ }
62
+ function suiteTarget(raw) {
63
+ if (raw != null && typeof raw === "object" && "target" in raw) {
64
+ const target = raw.target;
65
+ return typeof target === "string" ? target : void 0;
66
+ }
67
+ }
68
+ //#endregion
69
+ export { loadSuiteFile, renderSuiteTemplates };