@frontmcp/utils 1.12.0-rc.1 → 1.12.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.
@@ -2,38 +2,85 @@
2
2
  import { StoragePatternError } from "../errors.mjs";
3
3
  var MAX_PATTERN_LENGTH = 500;
4
4
  var MAX_WILDCARDS = 20;
5
- var REGEX_SPECIAL_CHARS = /[.+^${}()|[\]\\]/g;
6
- function globToRegex(pattern) {
5
+ var REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
6
+ function tokenizeGlob(pattern) {
7
7
  if (pattern.length > MAX_PATTERN_LENGTH) {
8
8
  throw new StoragePatternError(
9
9
  pattern.substring(0, 50) + "...",
10
10
  `Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH} characters`
11
11
  );
12
12
  }
13
- const wildcardCount = (pattern.match(/[*?]/g) || []).length;
13
+ const tokens = [];
14
+ let wildcardCount = 0;
15
+ for (let i = 0; i < pattern.length; i++) {
16
+ const char = pattern[i];
17
+ if (char === "\\" && i + 1 < pattern.length) {
18
+ i++;
19
+ tokens.push({ kind: "literal", char: pattern[i] });
20
+ } else if (char === "*") {
21
+ wildcardCount++;
22
+ if (tokens[tokens.length - 1]?.kind !== "star") tokens.push({ kind: "star" });
23
+ } else if (char === "?") {
24
+ wildcardCount++;
25
+ tokens.push({ kind: "one" });
26
+ } else {
27
+ tokens.push({ kind: "literal", char });
28
+ }
29
+ }
14
30
  if (wildcardCount > MAX_WILDCARDS) {
15
31
  throw new StoragePatternError(pattern, `Pattern has too many wildcards (max: ${MAX_WILDCARDS})`);
16
32
  }
17
- if (pattern === "" || pattern === "*") {
18
- return /^.*$/;
33
+ return tokens;
34
+ }
35
+ function matchesEverything(tokens) {
36
+ return tokens.length === 0 || tokens.length === 1 && tokens[0].kind === "star";
37
+ }
38
+ function matchTokens(tokens, key) {
39
+ let k = 0;
40
+ let t = 0;
41
+ let starToken = -1;
42
+ let starKey = 0;
43
+ while (k < key.length) {
44
+ const token = tokens[t];
45
+ if (token !== void 0 && (token.kind === "one" || token.kind === "literal" && token.char === key[k])) {
46
+ t++;
47
+ k++;
48
+ } else if (token !== void 0 && token.kind === "star") {
49
+ starToken = t;
50
+ starKey = k;
51
+ t++;
52
+ } else if (starToken !== -1) {
53
+ t = starToken + 1;
54
+ starKey++;
55
+ k = starKey;
56
+ } else {
57
+ return false;
58
+ }
59
+ }
60
+ while (t < tokens.length && tokens[t].kind === "star") t++;
61
+ return t === tokens.length;
62
+ }
63
+ function createGlobMatcher(pattern) {
64
+ const tokens = tokenizeGlob(pattern);
65
+ if (matchesEverything(tokens)) {
66
+ return () => true;
67
+ }
68
+ return (key) => matchTokens(tokens, key);
69
+ }
70
+ function globToRegex(pattern) {
71
+ const tokens = tokenizeGlob(pattern);
72
+ if (matchesEverything(tokens)) {
73
+ return /^[\s\S]*$/;
19
74
  }
20
75
  let regexStr = "^";
21
- let prevChar = "";
22
- for (let i = 0; i < pattern.length; i++) {
23
- const char = pattern[i];
24
- switch (char) {
25
- case "*":
26
- if (prevChar !== "*") {
27
- regexStr += ".*";
28
- }
29
- break;
30
- case "?":
31
- regexStr += ".";
32
- break;
33
- default:
34
- regexStr += char.replace(REGEX_SPECIAL_CHARS, "\\$&");
76
+ for (const token of tokens) {
77
+ if (token.kind === "star") {
78
+ regexStr += "[\\s\\S]*";
79
+ } else if (token.kind === "one") {
80
+ regexStr += "[\\s\\S]";
81
+ } else {
82
+ regexStr += token.char.replace(REGEX_SPECIAL_CHARS, "\\$&");
35
83
  }
36
- prevChar = char;
37
84
  }
38
85
  regexStr += "$";
39
86
  try {
@@ -43,12 +90,11 @@ function globToRegex(pattern) {
43
90
  }
44
91
  }
45
92
  function matchesPattern(key, pattern) {
46
- const regex = globToRegex(pattern);
47
- return regex.test(key);
93
+ return createGlobMatcher(pattern)(key);
48
94
  }
49
95
  function validatePattern(pattern) {
50
96
  try {
51
- globToRegex(pattern);
97
+ tokenizeGlob(pattern);
52
98
  return { valid: true };
53
99
  } catch (e) {
54
100
  return {
@@ -58,9 +104,10 @@ function validatePattern(pattern) {
58
104
  }
59
105
  }
60
106
  function escapeGlob(literal) {
61
- return literal.replace(/[*?\\]/g, "\\$&");
107
+ return literal.replace(/[*?[\]\\]/g, "\\$&");
62
108
  }
63
109
  export {
110
+ createGlobMatcher,
64
111
  escapeGlob,
65
112
  globToRegex,
66
113
  matchesPattern,
@@ -1,5 +1,12 @@
1
1
  // libs/utils/src/uri/uri-template.ts
2
2
  function parseUriTemplate(template) {
3
+ const parts = templateParts(template);
4
+ return {
5
+ pattern: new RegExp(`^${parts.map(regexSourceOf).join("")}$`),
6
+ paramNames: paramNamesOf(parts)
7
+ };
8
+ }
9
+ function templateParts(template) {
3
10
  if (template.length > 1e3) {
4
11
  throw new Error("URI template too long (max 1000 characters)");
5
12
  }
@@ -7,34 +14,105 @@ function parseUriTemplate(template) {
7
14
  if (paramCount > 50) {
8
15
  throw new Error("URI template has too many parameters (max 50)");
9
16
  }
10
- const paramNames = [];
17
+ const parts = [];
11
18
  const placeholder = /\{([^{}]+)\}/g;
12
- let regexStr = "";
13
19
  let lastIndex = 0;
14
20
  for (let m = placeholder.exec(template); m !== null; m = placeholder.exec(template)) {
15
- regexStr += escapeRegex(template.slice(lastIndex, m.index));
21
+ parts.push({ kind: "literal", text: template.slice(lastIndex, m.index) });
16
22
  const raw = m[1];
17
23
  const reserved = raw.startsWith("+");
18
- paramNames.push(reserved ? raw.slice(1) : raw);
19
- regexStr += reserved ? "(.+?)" : "([^/]+)";
24
+ parts.push({ kind: "param", name: reserved ? raw.slice(1) : raw, reserved });
20
25
  lastIndex = m.index + m[0].length;
21
26
  }
22
- regexStr += escapeRegex(template.slice(lastIndex));
23
- return {
24
- pattern: new RegExp(`^${regexStr}$`),
25
- paramNames
26
- };
27
+ parts.push({ kind: "literal", text: template.slice(lastIndex) });
28
+ return parts;
29
+ }
30
+ function regexSourceOf(part) {
31
+ if (part.kind === "literal") return escapeRegex(part.text);
32
+ return part.reserved ? "(.+?)" : "([^/]+)";
33
+ }
34
+ function paramNamesOf(parts) {
35
+ return parts.flatMap((part) => part.kind === "param" ? [part.name] : []);
27
36
  }
28
37
  function escapeRegex(literal) {
29
38
  return literal.replace(/[.*+?^${}()|[\]\\]/g, (c) => `\\${c}`);
30
39
  }
40
+ function matchTemplateParts(parts, uri) {
41
+ const fitsFrom = new Array(parts.length + 1);
42
+ fitsFrom[parts.length] = new PositionSet(uri.length);
43
+ fitsFrom[parts.length].add(uri.length);
44
+ for (let index = parts.length - 1; index >= 0; index--) {
45
+ fitsFrom[index] = positionsWherePartFits(parts[index], uri, fitsFrom[index + 1]);
46
+ if (parts[index].kind !== "param") fitsFrom[index + 1] = EMPTY_POSITIONS;
47
+ }
48
+ if (!fitsFrom[0].has(0)) return null;
49
+ const values = [];
50
+ let position = 0;
51
+ parts.forEach((part, index) => {
52
+ if (part.kind === "literal") {
53
+ position += part.text.length;
54
+ return;
55
+ }
56
+ const valueEnd = firstPositionNotHeld(part, uri, position);
57
+ const rest = fitsFrom[index + 1];
58
+ let end = part.reserved ? position + 1 : valueEnd;
59
+ while (!rest.has(end)) end += part.reserved ? 1 : -1;
60
+ values.push(uri.slice(position, end));
61
+ position = end;
62
+ });
63
+ return values;
64
+ }
65
+ function positionsWherePartFits(part, uri, rest) {
66
+ const fits = new PositionSet(uri.length);
67
+ if (part.kind === "literal") {
68
+ for (let position = 0; position + part.text.length <= uri.length; position++) {
69
+ if (rest.has(position + part.text.length) && uri.startsWith(part.text, position)) fits.add(position);
70
+ }
71
+ return fits;
72
+ }
73
+ let valueEnd = uri.length;
74
+ let nearestRest = Infinity;
75
+ for (let position = uri.length - 1; position >= 0; position--) {
76
+ if (rest.has(position + 1)) nearestRest = position + 1;
77
+ if (!canHold(part, uri.charCodeAt(position))) valueEnd = position;
78
+ if (nearestRest <= valueEnd) fits.add(position);
79
+ }
80
+ return fits;
81
+ }
82
+ function firstPositionNotHeld(part, uri, position) {
83
+ let end = position;
84
+ while (end < uri.length && canHold(part, uri.charCodeAt(end))) end++;
85
+ return end;
86
+ }
87
+ function canHold(part, charCode) {
88
+ if (part.kind !== "param") return false;
89
+ if (!part.reserved) return charCode !== SLASH;
90
+ return !LINE_TERMINATORS.has(charCode);
91
+ }
92
+ var SLASH = 47;
93
+ var LINE_TERMINATORS = /* @__PURE__ */ new Set([10, 13, 8232, 8233]);
94
+ var PositionSet = class {
95
+ bits;
96
+ constructor(length) {
97
+ this.bits = new Uint8Array((length >> 3) + 1);
98
+ }
99
+ /** Add `position`. */
100
+ add(position) {
101
+ this.bits[position >> 3] |= 1 << (position & 7);
102
+ }
103
+ /** Whether `position` was added. */
104
+ has(position) {
105
+ return position >= 0 && ((this.bits[position >> 3] ?? 0) & 1 << (position & 7)) !== 0;
106
+ }
107
+ };
108
+ var EMPTY_POSITIONS = new PositionSet(0);
31
109
  function matchUriTemplate(template, uri) {
32
- const { pattern, paramNames } = parseUriTemplate(template);
33
- const match = uri.match(pattern);
34
- if (!match) return null;
110
+ const parts = templateParts(template);
111
+ const values = matchTemplateParts(parts, uri);
112
+ if (!values) return null;
35
113
  const params = {};
36
- paramNames.forEach((name, index) => {
37
- params[name] = decodeURIComponent(match[index + 1]);
114
+ paramNamesOf(parts).forEach((name, index) => {
115
+ params[name] = decodeURIComponent(values[index]);
38
116
  });
39
117
  return params;
40
118
  }
@@ -102,7 +102,10 @@ export interface CreateKeyPersistenceOptions {
102
102
  /**
103
103
  * Optional 32-byte AES-256-GCM key for encrypting values at rest in localStorage.
104
104
  * Only used when type is 'localstorage' or 'auto' with localStorage fallback.
105
- * If not provided, HKDF-SHA256 derives a key from a fixed IKM using the page origin.
105
+ * If not provided, HKDF-SHA256 derives a key from a fixed IKM, a random salt kept in the same
106
+ * localStorage and the page origin: values are not readable at a glance, but anyone who can read
107
+ * this origin's localStorage can derive the key and decrypt them. To protect them from such a
108
+ * reader, pass a key kept outside localStorage.
106
109
  */
107
110
  encryptionKey?: Uint8Array;
108
111
  }
@@ -99,3 +99,22 @@ export declare function escapeScriptClose(jsonString: string): string;
99
99
  * ```
100
100
  */
101
101
  export declare function safeJsonForScript(value: unknown): string;
102
+ /**
103
+ * Escape control characters so that a value always stays on one line of text output.
104
+ *
105
+ * Use it on values that come from callers (names, URIs, identifiers, messages from a remote peer) before they go into
106
+ * a log line or a terminal: a raw line break would let the value forge a separate log record, and an escape character
107
+ * could rewrite the terminal. Line feed, carriage return, backspace, form feed and vertical tab become a backslash
108
+ * and `n`, `r`, `b`, `f` or `v`; other C0 and C1 controls, DEL, U+2028 and U+2029 become a backslash, `u` and four hex
109
+ * digits. Tabs and every other character are kept.
110
+ *
111
+ * @param value - Value to escape (converted to a string first; null and undefined become an empty string)
112
+ * @returns The value with every control character replaced by a visible escape
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * escapeControlCharacters('tool\nINFO forged entry'); // 'tool\\nINFO forged entry'
117
+ * escapeControlCharacters('\u001b[31mred'); // '\\u001b[31mred'
118
+ * ```
119
+ */
120
+ export declare function escapeControlCharacters(value: unknown): string;
package/escape/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript } from './escape';
1
+ export { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript, escapeControlCharacters, } from './escape';