specshield 3.2.2 → 3.2.4

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,100 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * JSON Schema inference from sample bodies — for HAR-ingest capture (Fix 2).
5
+ *
6
+ * inferSchema(value) → JSON Schema describing one sample
7
+ * mergeSchemas(a, b) → schema that admits both inputs
8
+ *
9
+ * The merger is the interesting bit: across multiple recorded responses
10
+ * for the same endpoint, fields seen in EVERY sample stay `required`;
11
+ * fields seen in only SOME become optional; type conflicts widen
12
+ * conservatively (integer + number → number; otherwise → string).
13
+ *
14
+ * Common string formats (uuid, date-time, email) are detected so the
15
+ * emitted OpenAPI subset is richer than just "type: string".
16
+ */
17
+
18
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ const DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
20
+ const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
21
+
22
+ function inferSchema(value) {
23
+ if (value === null || value === undefined) return { type: 'null' };
24
+ if (typeof value === 'boolean') return { type: 'boolean' };
25
+ if (typeof value === 'string') {
26
+ if (UUID_RE.test(value)) return { type: 'string', format: 'uuid' };
27
+ if (DATE_TIME_RE.test(value)) return { type: 'string', format: 'date-time' };
28
+ if (EMAIL_RE.test(value)) return { type: 'string', format: 'email' };
29
+ return { type: 'string' };
30
+ }
31
+ if (typeof value === 'number') {
32
+ return Number.isInteger(value) ? { type: 'integer' } : { type: 'number' };
33
+ }
34
+ if (Array.isArray(value)) {
35
+ if (value.length === 0) return { type: 'array', items: {} };
36
+ let items = inferSchema(value[0]);
37
+ for (let i = 1; i < value.length; i++) {
38
+ items = mergeSchemas(items, inferSchema(value[i]));
39
+ }
40
+ return { type: 'array', items };
41
+ }
42
+ if (typeof value === 'object') {
43
+ const properties = {};
44
+ const required = [];
45
+ for (const [k, v] of Object.entries(value)) {
46
+ properties[k] = inferSchema(v);
47
+ // null values still register the key, but the field is not "required"
48
+ // (it was present-but-null; another sample might omit it entirely).
49
+ if (v !== null && v !== undefined) required.push(k);
50
+ }
51
+ const out = { type: 'object', properties };
52
+ if (required.length > 0) out.required = required;
53
+ return out;
54
+ }
55
+ return {};
56
+ }
57
+
58
+ function mergeSchemas(a, b) {
59
+ if (!a || Object.keys(a).length === 0) return b || {};
60
+ if (!b || Object.keys(b).length === 0) return a;
61
+ if (a.type === 'null') return b;
62
+ if (b.type === 'null') return a;
63
+
64
+ if (a.type === b.type) {
65
+ if (a.type === 'object') {
66
+ const out = { type: 'object', properties: {} };
67
+ const allKeys = new Set([
68
+ ...Object.keys(a.properties || {}),
69
+ ...Object.keys(b.properties || {}),
70
+ ]);
71
+ for (const k of allKeys) {
72
+ const aProp = a.properties && a.properties[k];
73
+ const bProp = b.properties && b.properties[k];
74
+ out.properties[k] = aProp && bProp ? mergeSchemas(aProp, bProp) : (aProp || bProp);
75
+ }
76
+ // Required = INTERSECTION (a field is only "always present" if both samples had it).
77
+ const aReq = new Set(a.required || []);
78
+ const bReq = new Set(b.required || []);
79
+ const intersect = [...aReq].filter(k => bReq.has(k));
80
+ if (intersect.length > 0) out.required = intersect;
81
+ return out;
82
+ }
83
+ if (a.type === 'array') {
84
+ return { type: 'array', items: mergeSchemas(a.items || {}, b.items || {}) };
85
+ }
86
+ // Same primitive type. Keep `format` only if both samples agreed.
87
+ const out = { type: a.type };
88
+ if (a.format && a.format === b.format) out.format = a.format;
89
+ return out;
90
+ }
91
+
92
+ // Type mismatch — widen numerically; otherwise fall back to string.
93
+ if ((a.type === 'integer' && b.type === 'number') ||
94
+ (a.type === 'number' && b.type === 'integer')) {
95
+ return { type: 'number' };
96
+ }
97
+ return { type: 'string' };
98
+ }
99
+
100
+ module.exports = { inferSchema, mergeSchemas };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tolerate a leading `v` on user-supplied versions.
5
+ *
6
+ * The UI pill and several CLI displays render versions as `v<version>` for
7
+ * readability; readers routinely paste those back into lookup flags
8
+ * (`--version`, `--consumer-version`, `--provider-version`) where the leading
9
+ * `v` then silently makes the query miss every record because the stored
10
+ * value never has one.
11
+ *
12
+ * Strip a `v` (case-insensitive) ONLY when it sits in front of a digit, so
13
+ * legitimate strings that start with `v` followed by a letter (`vendor-tag`,
14
+ * `vNext`) pass through untouched.
15
+ *
16
+ * Applied at the entry of every LOOKUP action (`verify`, `can-i-deploy`).
17
+ * NOT applied to publish actions — the publisher's version is whatever they
18
+ * chose to store, including a literal `v` prefix if they want one.
19
+ */
20
+ function stripVersionPrefix(v) {
21
+ return typeof v === 'string' ? v.replace(/^v(?=\d)/i, '') : v;
22
+ }
23
+
24
+ module.exports = { stripVersionPrefix };