uriTemplate.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. "use strict";
  2. // Claude-authored implementation of RFC 6570 URI Templates
  3. Object.defineProperty(exports, "__esModule", { value: true });
  4. exports.UriTemplate = void 0;
  5. const MAX_TEMPLATE_LENGTH = 1000000; // 1MB
  6. const MAX_VARIABLE_LENGTH = 1000000; // 1MB
  7. const MAX_TEMPLATE_EXPRESSIONS = 10000;
  8. const MAX_REGEX_LENGTH = 1000000; // 1MB
  9. class UriTemplate {
  10. /**
  11. * Returns true if the given string contains any URI template expressions.
  12. * A template expression is a sequence of characters enclosed in curly braces,
  13. * like {foo} or {?bar}.
  14. */
  15. static isTemplate(str) {
  16. // Look for any sequence of characters between curly braces
  17. // that isn't just whitespace
  18. return /\{[^}\s]+\}/.test(str);
  19. }
  20. static validateLength(str, max, context) {
  21. if (str.length > max) {
  22. throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
  23. }
  24. }
  25. get variableNames() {
  26. return this.parts.flatMap(part => (typeof part === 'string' ? [] : part.names));
  27. }
  28. constructor(template) {
  29. UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template');
  30. this.template = template;
  31. this.parts = this.parse(template);
  32. }
  33. toString() {
  34. return this.template;
  35. }
  36. parse(template) {
  37. const parts = [];
  38. let currentText = '';
  39. let i = 0;
  40. let expressionCount = 0;
  41. while (i < template.length) {
  42. if (template[i] === '{') {
  43. if (currentText) {
  44. parts.push(currentText);
  45. currentText = '';
  46. }
  47. const end = template.indexOf('}', i);
  48. if (end === -1)
  49. throw new Error('Unclosed template expression');
  50. expressionCount++;
  51. if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
  52. throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
  53. }
  54. const expr = template.slice(i + 1, end);
  55. const operator = this.getOperator(expr);
  56. const exploded = expr.includes('*');
  57. const names = this.getNames(expr);
  58. const name = names[0];
  59. // Validate variable name length
  60. for (const name of names) {
  61. UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
  62. }
  63. parts.push({ name, operator, names, exploded });
  64. i = end + 1;
  65. }
  66. else {
  67. currentText += template[i];
  68. i++;
  69. }
  70. }
  71. if (currentText) {
  72. parts.push(currentText);
  73. }
  74. return parts;
  75. }
  76. getOperator(expr) {
  77. const operators = ['+', '#', '.', '/', '?', '&'];
  78. return operators.find(op => expr.startsWith(op)) || '';
  79. }
  80. getNames(expr) {
  81. const operator = this.getOperator(expr);
  82. return expr
  83. .slice(operator.length)
  84. .split(',')
  85. .map(name => name.replace('*', '').trim())
  86. .filter(name => name.length > 0);
  87. }
  88. encodeValue(value, operator) {
  89. UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, 'Variable value');
  90. if (operator === '+' || operator === '#') {
  91. return encodeURI(value);
  92. }
  93. return encodeURIComponent(value);
  94. }
  95. expandPart(part, variables) {
  96. if (part.operator === '?' || part.operator === '&') {
  97. const pairs = part.names
  98. .map(name => {
  99. const value = variables[name];
  100. if (value === undefined)
  101. return '';
  102. const encoded = Array.isArray(value)
  103. ? value.map(v => this.encodeValue(v, part.operator)).join(',')
  104. : this.encodeValue(value.toString(), part.operator);
  105. return `${name}=${encoded}`;
  106. })
  107. .filter(pair => pair.length > 0);
  108. if (pairs.length === 0)
  109. return '';
  110. const separator = part.operator === '?' ? '?' : '&';
  111. return separator + pairs.join('&');
  112. }
  113. if (part.names.length > 1) {
  114. const values = part.names.map(name => variables[name]).filter(v => v !== undefined);
  115. if (values.length === 0)
  116. return '';
  117. return values.map(v => (Array.isArray(v) ? v[0] : v)).join(',');
  118. }
  119. const value = variables[part.name];
  120. if (value === undefined)
  121. return '';
  122. const values = Array.isArray(value) ? value : [value];
  123. const encoded = values.map(v => this.encodeValue(v, part.operator));
  124. switch (part.operator) {
  125. case '':
  126. return encoded.join(',');
  127. case '+':
  128. return encoded.join(',');
  129. case '#':
  130. return '#' + encoded.join(',');
  131. case '.':
  132. return '.' + encoded.join('.');
  133. case '/':
  134. return '/' + encoded.join('/');
  135. default:
  136. return encoded.join(',');
  137. }
  138. }
  139. expand(variables) {
  140. let result = '';
  141. let hasQueryParam = false;
  142. for (const part of this.parts) {
  143. if (typeof part === 'string') {
  144. result += part;
  145. continue;
  146. }
  147. const expanded = this.expandPart(part, variables);
  148. if (!expanded)
  149. continue;
  150. // Convert ? to & if we already have a query parameter
  151. if ((part.operator === '?' || part.operator === '&') && hasQueryParam) {
  152. result += expanded.replace('?', '&');
  153. }
  154. else {
  155. result += expanded;
  156. }
  157. if (part.operator === '?' || part.operator === '&') {
  158. hasQueryParam = true;
  159. }
  160. }
  161. return result;
  162. }
  163. escapeRegExp(str) {
  164. return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  165. }
  166. partToRegExp(part) {
  167. const patterns = [];
  168. // Validate variable name length for matching
  169. for (const name of part.names) {
  170. UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
  171. }
  172. if (part.operator === '?' || part.operator === '&') {
  173. for (let i = 0; i < part.names.length; i++) {
  174. const name = part.names[i];
  175. const prefix = i === 0 ? '\\' + part.operator : '&';
  176. patterns.push({
  177. pattern: prefix + this.escapeRegExp(name) + '=([^&]+)',
  178. name
  179. });
  180. }
  181. return patterns;
  182. }
  183. let pattern;
  184. const name = part.name;
  185. switch (part.operator) {
  186. case '':
  187. pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)';
  188. break;
  189. case '+':
  190. case '#':
  191. pattern = '(.+)';
  192. break;
  193. case '.':
  194. pattern = '\\.([^/,]+)';
  195. break;
  196. case '/':
  197. pattern = '/' + (part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)');
  198. break;
  199. default:
  200. pattern = '([^/]+)';
  201. }
  202. patterns.push({ pattern, name });
  203. return patterns;
  204. }
  205. match(uri) {
  206. UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI');
  207. let pattern = '^';
  208. const names = [];
  209. for (const part of this.parts) {
  210. if (typeof part === 'string') {
  211. pattern += this.escapeRegExp(part);
  212. }
  213. else {
  214. const patterns = this.partToRegExp(part);
  215. for (const { pattern: partPattern, name } of patterns) {
  216. pattern += partPattern;
  217. names.push({ name, exploded: part.exploded });
  218. }
  219. }
  220. }
  221. pattern += '$';
  222. UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');
  223. const regex = new RegExp(pattern);
  224. const match = uri.match(regex);
  225. if (!match)
  226. return null;
  227. const result = {};
  228. for (let i = 0; i < names.length; i++) {
  229. const { name, exploded } = names[i];
  230. const value = match[i + 1];
  231. const cleanName = name.replace('*', '');
  232. if (exploded && value.includes(',')) {
  233. result[cleanName] = value.split(',');
  234. }
  235. else {
  236. result[cleanName] = value;
  237. }
  238. }
  239. return result;
  240. }
  241. }
  242. exports.UriTemplate = UriTemplate;
  243. //# sourceMappingURL=uriTemplate.js.map