@suveren/gateway 0.29.0 → 0.30.0

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 (34) hide show
  1. package/content/integrations/gmail.json +13 -2
  2. package/content/integrations/mail.json +12 -2
  3. package/content/integrations/mollie.json +10 -0
  4. package/dist/control-plane/index.mjs +125 -37
  5. package/dist/mcp-server/http.mjs +772 -420
  6. package/dist/ui/assets/index-C2BnlNib.js +123 -0
  7. package/dist/ui/assets/{index-DUIlMVlP.css → index-DPgewsLG.css} +1 -1
  8. package/dist/ui/index.html +2 -2
  9. package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
  10. package/node_modules/qs/CHANGELOG.md +38 -14
  11. package/node_modules/qs/README.md +31 -3
  12. package/node_modules/qs/dist/qs.js +20 -20
  13. package/node_modules/qs/lib/parse.js +94 -4
  14. package/node_modules/qs/lib/stringify.js +4 -4
  15. package/node_modules/qs/lib/utils.js +130 -6
  16. package/node_modules/qs/package.json +4 -4
  17. package/node_modules/qs/test/parse.js +174 -0
  18. package/node_modules/qs/test/stringify.js +263 -33
  19. package/node_modules/qs/test/utils.js +184 -0
  20. package/node_modules/sanitize-html/index.js +47 -10
  21. package/node_modules/sanitize-html/package.json +2 -3
  22. package/package.json +1 -1
  23. package/dist/ui/assets/index-C3vTF2Ye.js +0 -123
  24. package/node_modules/deepmerge/.editorconfig +0 -7
  25. package/node_modules/deepmerge/.eslintcache +0 -1
  26. package/node_modules/deepmerge/changelog.md +0 -167
  27. package/node_modules/deepmerge/dist/cjs.js +0 -133
  28. package/node_modules/deepmerge/dist/umd.js +0 -139
  29. package/node_modules/deepmerge/index.d.ts +0 -20
  30. package/node_modules/deepmerge/index.js +0 -106
  31. package/node_modules/deepmerge/license.txt +0 -21
  32. package/node_modules/deepmerge/package.json +0 -42
  33. package/node_modules/deepmerge/readme.md +0 -264
  34. package/node_modules/deepmerge/rollup.config.js +0 -22
@@ -106,6 +106,146 @@ test('merge()', function (t) {
106
106
  s2t.end();
107
107
  });
108
108
 
109
+ st.test('with strictMerge, repeated conflicts append instead of nesting a new wrapper', function (s2t) {
110
+ var opts = { strictMerge: true };
111
+ var wrapped = utils.merge({ foo: 'bar' }, 'baz', opts);
112
+ s2t.deepEqual(wrapped, [{ foo: 'bar' }, 'baz'], 'first conflict wraps in an array');
113
+
114
+ var withObject = utils.merge(wrapped, { qux: 'quux' }, opts);
115
+ s2t.deepEqual(
116
+ withObject,
117
+ { 0: { foo: 'bar' }, 1: 'baz', qux: 'quux' },
118
+ 'merging an object converts the wrapper to an object'
119
+ );
120
+
121
+ var second = utils.merge(withObject, 'corge', opts);
122
+ s2t.deepEqual(
123
+ second,
124
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
125
+ 'a second conflict appends at the next index'
126
+ );
127
+
128
+ var third = utils.merge(second, 'grault', opts);
129
+ s2t.deepEqual(
130
+ third,
131
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', 3: 'grault', qux: 'quux' },
132
+ 'a third conflict appends again, so nesting depth stays constant'
133
+ );
134
+
135
+ s2t.end();
136
+ });
137
+
138
+ st.test('with strictMerge, a caller-supplied array is still wrapped, not appended to', function (s2t) {
139
+ var merged = utils.merge({ 0: 'a', 1: 'b' }, 'c', { strictMerge: true });
140
+ s2t.deepEqual(merged, [{ 0: 'a', 1: 'b' }, 'c'], 'an unmarked object with numeric keys is wrapped');
141
+ s2t.end();
142
+ });
143
+
144
+ st.test('with strictMerge, throws instead of appending a conflict past arrayLimit with throwOnLimitExceeded', function (s2t) {
145
+ var appendConflict = function (options) {
146
+ var wrapped = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true });
147
+ var container = utils.merge(wrapped, { qux: 'quux' }, { strictMerge: true });
148
+ return utils.merge(container, 'corge', options);
149
+ };
150
+
151
+ s2t['throws'](
152
+ function () { appendConflict({ strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
153
+ new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
154
+ 'appending to a two-element container past arrayLimit throws'
155
+ );
156
+ s2t['throws'](
157
+ function () { appendConflict({ strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }); },
158
+ new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
159
+ 'uses the plural message when arrayLimit is not 1'
160
+ );
161
+ s2t.deepEqual(
162
+ appendConflict({ strictMerge: true, arrayLimit: 3, throwOnLimitExceeded: true }),
163
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
164
+ 'appends while the container stays within arrayLimit'
165
+ );
166
+ s2t.deepEqual(
167
+ appendConflict({ strictMerge: true, arrayLimit: 1 }),
168
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
169
+ 'still appends past arrayLimit without throwOnLimitExceeded, since the container is already an object'
170
+ );
171
+
172
+ s2t.end();
173
+ });
174
+
175
+ st.test('with strictMerge, a conflict appends after numeric keys already in its container', function (s2t) {
176
+ var opts = { strictMerge: true };
177
+
178
+ var container = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), { qux: 'quux' }, opts);
179
+ var withIndex = utils.merge(container, { 2: 'keep' }, opts);
180
+ s2t.deepEqual(
181
+ utils.merge(withIndex, 'corge', opts),
182
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
183
+ 'a numeric key merged into the container is not overwritten'
184
+ );
185
+
186
+ var sparse = [];
187
+ sparse[2] = 'keep';
188
+ var extended = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), sparse, opts);
189
+ s2t.deepEqual(extended, [{ foo: 'bar' }, 'baz', 'keep'], 'an indexed value extends the container while it is an array');
190
+ var converted = utils.merge(extended, { qux: 'quux' }, opts);
191
+ s2t.deepEqual(
192
+ utils.merge(converted, 'corge', opts),
193
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
194
+ 'converting the container to an object keeps its last index'
195
+ );
196
+
197
+ s2t.end();
198
+ });
199
+
200
+ st.test('enforces arrayLimit on the two-element array built for colliding values', function (s2t) {
201
+ s2t['throws'](
202
+ function () { utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
203
+ new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
204
+ 'a strictMerge conflict throws at arrayLimit 1'
205
+ );
206
+ s2t['throws'](
207
+ function () { utils.merge('a', 'b', { arrayLimit: 0, throwOnLimitExceeded: true }); },
208
+ new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
209
+ 'two colliding primitives throw at arrayLimit 0'
210
+ );
211
+
212
+ var conflict = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1 });
213
+ s2t.ok(utils.isOverflow(conflict), 'without throwOnLimitExceeded, a strictMerge conflict converts to an overflow object');
214
+ s2t.deepEqual(conflict, { 0: { foo: 'bar' }, 1: 'baz' }, 'holding both values');
215
+
216
+ var pair = utils.merge('a', 'b', { arrayLimit: 1 });
217
+ s2t.ok(utils.isOverflow(pair), 'without throwOnLimitExceeded, two colliding primitives convert to an overflow object');
218
+ s2t.deepEqual(pair, { 0: 'a', 1: 'b' }, 'holding both values');
219
+
220
+ s2t.deepEqual(
221
+ utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }),
222
+ [{ foo: 'bar' }, 'baz'],
223
+ 'a strictMerge conflict within arrayLimit is still an array'
224
+ );
225
+ s2t.deepEqual(
226
+ utils.merge('a', 'b', { arrayLimit: 2, throwOnLimitExceeded: true }),
227
+ ['a', 'b'],
228
+ 'two colliding primitives within arrayLimit are still an array'
229
+ );
230
+
231
+ s2t.end();
232
+ });
233
+
234
+ st.test('seeds an overflow container index from the keys it already holds', function (s2t) {
235
+ var overflow = utils.combine(['a'], 'b', 0, false);
236
+ s2t.ok(utils.isOverflow(overflow), 'the source is an overflow object');
237
+
238
+ var merged = utils.merge({ 2: 'keep', z: 'zz' }, overflow);
239
+ s2t.ok(utils.isOverflow(merged), 'the result is marked as overflow');
240
+ s2t.deepEqual(
241
+ utils.merge(merged, 'c'),
242
+ { 0: 'a', 1: 'b', 2: 'keep', 3: 'c', z: 'zz' },
243
+ 'the next value lands after the highest numeric key the container already held'
244
+ );
245
+
246
+ s2t.end();
247
+ });
248
+
109
249
  st.test('merges overflow object into primitive', function (s2t) {
110
250
  // Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
111
251
  var overflow = utils.combine(['a'], 'b', 0, false);
@@ -441,6 +581,50 @@ test('decode', function (t) {
441
581
  'decodes a string in iso-8859-1'
442
582
  );
443
583
 
584
+ t.test('strings with no percent sign', function (st) {
585
+ st.equal(utils.decode(''), '', 'empty string');
586
+ st.equal(utils.decode('abc'), 'abc', 'returns a plain string unchanged');
587
+ st.equal(utils.decode('abc', null, 'iso-8859-1'), 'abc', 'returns a plain string unchanged in iso-8859-1');
588
+ st.equal(utils.decode('a++b+'), 'a b ', 'decodes every + to a space');
589
+ st.equal(utils.decode('a++b+', null, 'iso-8859-1'), 'a b ', 'decodes every + to a space in iso-8859-1');
590
+ st.equal(utils.decode('\uD800x\uDC00'), '\uD800x\uDC00', 'leaves lone surrogates unchanged');
591
+ st.equal(utils.decode('😀+'), '😀 ', 'leaves surrogate pairs unchanged');
592
+ st.equal(utils.decode('a=b&c'), 'a=b&c', 'leaves delimiters unchanged');
593
+
594
+ st.end();
595
+ });
596
+
597
+ t.test('strings with a percent sign', function (st) {
598
+ st.equal(utils.decode('a+%20b'), 'a b', 'decodes both + and an escape');
599
+ st.equal(utils.decode('%E9+x', null, 'iso-8859-1'), 'é x', 'decodes both + and an escape in iso-8859-1');
600
+ st.equal(utils.decode('a+%'), 'a %', 'decodes + even when the escape is malformed');
601
+ st.equal(utils.decode('a+%E0%A4%A'), 'a %E0%A4%A', 'decodes + even when the escape is truncated');
602
+
603
+ st.end();
604
+ });
605
+
606
+ t.test('non-strings', function (st) {
607
+ forEach(v.nullPrimitives, function (nullish) {
608
+ st['throws'](
609
+ function () { utils.decode(nullish); },
610
+ TypeError,
611
+ inspect(nullish) + ' is not a string'
612
+ );
613
+ });
614
+ st['throws'](
615
+ function () { utils.decode(['a']); },
616
+ TypeError,
617
+ 'an array is not a string'
618
+ );
619
+
620
+ /* eslint no-new-wrappers: 0 */
621
+ st.equal(utils.decode(new String('ab')), 'ab', 'a boxed string decodes to a primitive');
622
+ st.equal(utils.decode(new String('a+b')), 'a b', 'a boxed string with + decodes to a primitive');
623
+ st.equal(utils.decode(new String('a%20b')), 'a b', 'a boxed string with an escape decodes to a primitive');
624
+
625
+ st.end();
626
+ });
627
+
444
628
  t.end();
445
629
  });
446
630
 
@@ -1,7 +1,6 @@
1
1
  const htmlparser = require('htmlparser2');
2
2
  const escapeStringRegexp = require('escape-string-regexp');
3
3
  const { isPlainObject } = require('is-plain-object');
4
- const deepmerge = require('deepmerge');
5
4
  const parseSrcset = require('parse-srcset');
6
5
  const { parse: postcssParse } = require('postcss');
7
6
  const { naughtyHref: launderNaughtyHref } = require('launder');
@@ -53,6 +52,29 @@ function has(obj, key) {
53
52
  return ({}).hasOwnProperty.call(obj, key);
54
53
  }
55
54
 
55
+ // Combine the tag-specific and wildcard lists of allowed classes
56
+ function mergeClasses(specific, wildcard) {
57
+ if (!Array.isArray(specific) || !Array.isArray(wildcard)) {
58
+ return wildcard;
59
+ }
60
+ return specific.concat(wildcard);
61
+ }
62
+
63
+ // Combine the tag-specific and wildcard allowed styles, concatenating the
64
+ // lists of regular expressions for properties that appear in both. The result
65
+ // has no prototype, so a property named `__proto__` is just another property
66
+ function mergeStyles(specific, wildcard) {
67
+ const merged = Object.create(null);
68
+ for (const rules of [ specific, wildcard ]) {
69
+ for (const prop of Object.keys(rules)) {
70
+ merged[prop] = has(merged, prop)
71
+ ? merged[prop].concat(rules[prop])
72
+ : rules[prop];
73
+ }
74
+ }
75
+ return merged;
76
+ }
77
+
56
78
  // Returns those elements of `a` for which `cb(a)` returns truthy
57
79
  function filter(a, cb) {
58
80
  const n = [];
@@ -103,14 +125,23 @@ module.exports = sanitizeHtml;
103
125
  // attribute name state with an empty attribute name buffer.
104
126
  const VALID_HTML_ATTRIBUTE_NAME = /^[^\0\t\n\f\r /<=>]+$/;
105
127
 
106
- // Ignore the _recursing flag; it's there for recursive
107
- // invocation as a guard against this exploit:
108
- // https://github.com/fb55/htmlparser2/issues/105
128
+ // How many discarded <iframe> elements can be nested while their fallback
129
+ // markup is still re-sanitized as HTML (see the `ontext` handler). Deeper
130
+ // fallback is escaped as text, so that crafted input cannot recurse without
131
+ // bound or re-parse the remainder of the input once per iframe
132
+ // (GHSA-c9rx-x947-fp3h).
133
+ const MAX_IFRAME_FALLBACK_DEPTH = 8;
109
134
 
110
- function sanitizeHtml(html, options, _recursing) {
135
+ // `_fallback` is internal: it is set when re-sanitizing the fallback markup
136
+ // of a discarded <iframe>, to carry the current nesting depth into the
137
+ // recursive call.
138
+
139
+ function sanitizeHtml(html, options, _fallback) {
111
140
  if (html == null) {
112
141
  return '';
113
142
  }
143
+ const baseDepth = (_fallback && _fallback.depth) || 0;
144
+ const iframeFallbackDepth = (_fallback && _fallback.iframeFallbackDepth) || 0;
114
145
  if (typeof html === 'number') {
115
146
  html = html.toString();
116
147
  }
@@ -542,7 +573,7 @@ function sanitizeHtml(html, options, _recursing) {
542
573
  if (allowedSpecificClasses && allowedWildcardClasses) {
543
574
  value = filterClasses(
544
575
  value,
545
- deepmerge(allowedSpecificClasses, allowedWildcardClasses),
576
+ mergeClasses(allowedSpecificClasses, allowedWildcardClasses),
546
577
  allowedClassesGlobs
547
578
  );
548
579
  } else {
@@ -682,11 +713,17 @@ function sanitizeHtml(html, options, _recursing) {
682
713
  // (including after an unclosed <iframe>) arrives as a single text node.
683
714
  // When the iframe is discarded, re-sanitize that fallback markup as HTML
684
715
  // instead of escaping it as plain text (issue #5550).
716
+ // The nested call continues from the current depth so that
717
+ // `nestingLimit` applies to the whole output.
685
718
  tag === 'iframe' &&
686
719
  !tagAllowed(tag) &&
687
- options.disallowedTagsMode === 'discard'
720
+ options.disallowedTagsMode === 'discard' &&
721
+ iframeFallbackDepth < MAX_IFRAME_FALLBACK_DEPTH
688
722
  ) {
689
- result += sanitizeHtml(text, options);
723
+ result += sanitizeHtml(text, options, {
724
+ depth,
725
+ iframeFallbackDepth: iframeFallbackDepth + 1
726
+ });
690
727
  } else if (!addedText) {
691
728
  const escaped = escapeHtml(text, false);
692
729
  if (options.textFilter) {
@@ -819,7 +856,7 @@ function sanitizeHtml(html, options, _recursing) {
819
856
 
820
857
  function initializeState() {
821
858
  result = '';
822
- depth = 0;
859
+ depth = baseDepth;
823
860
  stack = [];
824
861
  skipMap = {};
825
862
  transformMap = {};
@@ -1053,7 +1090,7 @@ function sanitizeHtml(html, options, _recursing) {
1053
1090
 
1054
1091
  // Merge global and tag-specific styles into new AST.
1055
1092
  if (allowedStyles[astRules.selector] && allowedStyles['*']) {
1056
- selectedRule = deepmerge(
1093
+ selectedRule = mergeStyles(
1057
1094
  allowedStyles[astRules.selector],
1058
1095
  allowedStyles['*']
1059
1096
  );
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.18.0",
3
+ "version": "2.18.1",
4
4
  "description": "Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis",
5
5
  "sideEffects": false,
6
6
  "main": "index.js",
@@ -29,7 +29,6 @@
29
29
  "node": ">=22.12.0"
30
30
  },
31
31
  "dependencies": {
32
- "deepmerge": "^4.3.1",
33
32
  "escape-string-regexp": "^4.0.0",
34
33
  "htmlparser2": "^12.0.0",
35
34
  "is-plain-object": "^5.1.0",
@@ -40,7 +39,7 @@
40
39
  "devDependencies": {
41
40
  "eslint": "^9.39.5",
42
41
  "eslint-config-apostrophe": "^6.1.0",
43
- "mocha": "^11.8.0",
42
+ "mocha": "^12.0.3",
44
43
  "sinon": "^9.2.4"
45
44
  },
46
45
  "apostropheTestConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suveren/gateway",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Suveren gateway — local agent gateway built in compliance with the Human Agency Protocol (HAP). Runs the UI, control plane, and MCP server in one Node process.",
5
5
  "type": "module",
6
6
  "main": "server.js",