@pathmode/mcp-server 1.20.0 → 1.20.2

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.
package/dist/index.js CHANGED
@@ -35783,6 +35783,7 @@ function State(input, options) {
35783
35783
  this.legacy = options['legacy'] || false;
35784
35784
  this.json = options['json'] || false;
35785
35785
  this.listener = options['listener'] || null;
35786
+ this.maxTotalMergeKeys = typeof options['maxTotalMergeKeys'] === 'number' ? options['maxTotalMergeKeys'] : 10000;
35786
35787
 
35787
35788
  this.implicitTypes = this.schema.compiledImplicit;
35788
35789
  this.typeMap = this.schema.compiledTypeMap;
@@ -35792,6 +35793,7 @@ function State(input, options) {
35792
35793
  this.line = 0;
35793
35794
  this.lineStart = 0;
35794
35795
  this.lineIndent = 0;
35796
+ this.totalMergeKeys = 0;
35795
35797
 
35796
35798
  this.documents = [];
35797
35799
 
@@ -35910,6 +35912,14 @@ function captureSegment(state, start, end, checkJson) {
35910
35912
  }
35911
35913
  }
35912
35914
 
35915
+ function chargeMergeWork(state) {
35916
+ state.totalMergeKeys += 1;
35917
+
35918
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
35919
+ throwError(state, 'merge keys exceeded maxTotalMergeKeys (' + state.maxTotalMergeKeys + ')');
35920
+ }
35921
+ }
35922
+
35913
35923
  function mergeMappings(state, destination, source, overridableKeys) {
35914
35924
  var sourceKeys, key, index, quantity;
35915
35925
 
@@ -35917,11 +35927,16 @@ function mergeMappings(state, destination, source, overridableKeys) {
35917
35927
  throwError(state, 'cannot merge mappings; the provided source object is unacceptable');
35918
35928
  }
35919
35929
 
35930
+ // Count the source mapping itself to bound sequences of empty mappings.
35931
+ chargeMergeWork(state);
35932
+
35920
35933
  sourceKeys = Object.keys(source);
35921
35934
 
35922
35935
  for (index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
35923
35936
  key = sourceKeys[index];
35924
35937
 
35938
+ chargeMergeWork(state);
35939
+
35925
35940
  if (!_hasOwnProperty.call(destination, key)) {
35926
35941
  setProperty(destination, key, source[key]);
35927
35942
  overridableKeys[key] = true;
@@ -35965,6 +35980,10 @@ function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valu
35965
35980
 
35966
35981
  if (keyTag === 'tag:yaml.org,2002:merge') {
35967
35982
  if (Array.isArray(valueNode)) {
35983
+ if (valueNode.length > 100) {
35984
+ throwError(state, 'abnormal merge sequence size');
35985
+ }
35986
+
35968
35987
  for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {
35969
35988
  mergeMappings(state, _result, valueNode[index], overridableKeys);
35970
35989
  }
@@ -38504,7 +38523,7 @@ var _toString = Object.prototype.toString;
38504
38523
  function resolveYamlOmap(data) {
38505
38524
  if (data === null) return true;
38506
38525
 
38507
- var objectKeys = [], index, length, pair, pairKey, pairHasKey,
38526
+ var objectKeys = {}, index, length, pair, pairKey, pairHasKey,
38508
38527
  object = data;
38509
38528
 
38510
38529
  for (index = 0, length = object.length; index < length; index += 1) {
@@ -38522,8 +38541,8 @@ function resolveYamlOmap(data) {
38522
38541
 
38523
38542
  if (!pairHasKey) return false;
38524
38543
 
38525
- if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey);
38526
- else return false;
38544
+ if (_hasOwnProperty.call(objectKeys, pairKey)) return false;
38545
+ Object.defineProperty(objectKeys, pairKey, { value: true });
38527
38546
  }
38528
38547
 
38529
38548
  return true;
@@ -39199,6 +39218,7 @@ module.exports = function(str) {
39199
39218
  * the results in as plain data.
39200
39219
  */
39201
39220
  Object.defineProperty(exports, "__esModule", ({ value: true }));
39221
+ exports.renderOutcomeMeasurementDefinition = renderOutcomeMeasurementDefinition;
39202
39222
  exports.toSerializableChecks = toSerializableChecks;
39203
39223
  exports.renderAuthorizationGateText = renderAuthorizationGateText;
39204
39224
  exports.renderConfirmationsBody = renderConfirmationsBody;
@@ -39226,6 +39246,24 @@ function yamlScalar(value) {
39226
39246
  function outcomeOf(o) {
39227
39247
  return typeof o === 'string' ? { text: o } : o;
39228
39248
  }
39249
+ /** Compact, provider-neutral wording shared by every agent-facing export. */
39250
+ function renderOutcomeMeasurementDefinition(measurement) {
39251
+ if (!measurement)
39252
+ return '';
39253
+ const { source, expectation, window } = measurement;
39254
+ const target = `${expectation.operator} ${expectation.target}${expectation.unit ? ` ${expectation.unit}` : ''}`;
39255
+ const windowValue = window.kind === 'rolling'
39256
+ ? `${window.kind} ${window.duration}`
39257
+ : window.kind === 'fixed'
39258
+ ? `${window.kind} ${window.start}..${window.end}`
39259
+ : window.kind;
39260
+ return [
39261
+ `measure via ${source.provider} ${source.queryRef.kind}:${source.queryRef.id}`,
39262
+ measurement.resultSelector ? `select ${measurement.resultSelector}` : '',
39263
+ `target ${target}`,
39264
+ `window ${windowValue}`,
39265
+ ].filter(Boolean).join('; ');
39266
+ }
39229
39267
  function constraintOf(c) {
39230
39268
  return typeof c === 'string' ? { text: c } : c;
39231
39269
  }
@@ -39337,9 +39375,14 @@ function renderConfirmationsBody(records) {
39337
39375
  */
39338
39376
  const FRONTMATTER_ORDER = [
39339
39377
  'id', 'version', 'status', 'readiness', 'source', 'specVersion',
39340
- 'origin', 'authorization', 'authorizationNote', 'evidence', 'space', 'severity', 'created', 'updated',
39378
+ 'origin', 'authorization', 'authorizationNote', 'outcomeMeasurements',
39379
+ 'evidence', 'space', 'severity', 'created', 'updated',
39341
39380
  ];
39342
39381
  function serializeIntentMd(spec, opts = {}) {
39382
+ const outcomeMeasurements = (spec.outcomes || [])
39383
+ .map(outcomeOf)
39384
+ .map((outcome, index) => outcome.measurement ? { outcome: index, ...outcome.measurement } : null)
39385
+ .filter((entry) => entry !== null);
39343
39386
  const values = {
39344
39387
  id: spec.id,
39345
39388
  version: opts.version && opts.version >= 1 ? opts.version : 1,
@@ -39358,6 +39401,9 @@ function serializeIntentMd(spec, opts = {}) {
39358
39401
  authorizationNote: opts.origin === 'agent' && nonEmpty(opts.authorizationNote)
39359
39402
  ? opts.authorizationNote.replace(/\s+/g, ' ').trim()
39360
39403
  : undefined,
39404
+ // Body outcomes stay plain, interoperable list items. The optional recipe is a generic
39405
+ // extension in frontmatter, keyed by outcome position so old readers simply ignore it.
39406
+ outcomeMeasurements: outcomeMeasurements.length ? outcomeMeasurements : undefined,
39361
39407
  evidence: opts.evidence?.length || undefined,
39362
39408
  space: opts.space?.name,
39363
39409
  severity: opts.severity,
@@ -39369,7 +39415,10 @@ function serializeIntentMd(spec, opts = {}) {
39369
39415
  // crash rather than an omitted key.
39370
39416
  const yamlLines = FRONTMATTER_ORDER
39371
39417
  .filter(key => values[key] !== undefined && values[key] !== null && values[key] !== '')
39372
- .map(key => `${key}: ${yamlScalar(values[key])}`)
39418
+ .map(key => {
39419
+ const value = values[key];
39420
+ return `${key}: ${typeof value === 'object' ? JSON.stringify(value) : yamlScalar(value)}`;
39421
+ })
39373
39422
  .join('\n');
39374
39423
  const sections = ['---', yamlLines, '---', ''];
39375
39424
  // Directly under the frontmatter: the `authorization` key is machine-readable, but the body
@@ -39615,6 +39664,9 @@ async function exchangeCode(code, origin, existing) {
39615
39664
  }
39616
39665
  /** Add cloud identity to the existing frontmatter without reformatting the user's spec body. */
39617
39666
  function patchAdoptedIntentFrontmatter(filePath, values) {
39667
+ if (fs_1.default.lstatSync(filePath).isSymbolicLink()) {
39668
+ throw new Error('Refusing to update a symbolic-link intent.md. Use a regular file inside the repository.');
39669
+ }
39618
39670
  const raw = fs_1.default.readFileSync(filePath, 'utf8');
39619
39671
  const newline = raw.includes('\r\n') ? '\r\n' : '\n';
39620
39672
  if (!raw.startsWith(`---${newline}`)) {
@@ -39646,6 +39698,9 @@ async function runAdopt(argv = process.argv) {
39646
39698
  if (!code)
39647
39699
  throw new Error('Usage: npx @pathmode/mcp-server@latest adopt pm_adopt_…');
39648
39700
  const filePath = path_1.default.join(process.cwd(), 'intent.md');
39701
+ if (fs_1.default.existsSync(filePath) && fs_1.default.lstatSync(filePath).isSymbolicLink()) {
39702
+ throw new Error('Refusing to adopt a symbolic-link intent.md. Use a regular file inside the repository.');
39703
+ }
39649
39704
  const intent = (0, local_reader_1.readIntentFile)(filePath);
39650
39705
  const meta = (0, local_reader_1.readIntentMeta)(filePath);
39651
39706
  if (!intent || !meta?.id) {
@@ -39654,7 +39709,7 @@ async function runAdopt(argv = process.argv) {
39654
39709
  console.log('');
39655
39710
  console.log('Pathmode repo adoption');
39656
39711
  console.log('──────────────────────');
39657
- console.log(` Found: ${intent.title}`);
39712
+ console.log(` Read intent.md — ${intent.title}`);
39658
39713
  process.stdout.write(' Connecting…');
39659
39714
  const origin = apiOrigin(argv);
39660
39715
  const current = (0, api_client_1.loadConfig)();
@@ -39674,6 +39729,8 @@ async function runAdopt(argv = process.argv) {
39674
39729
  throw error;
39675
39730
  }
39676
39731
  }
39732
+ console.log(' ✓');
39733
+ console.log(` ✓ Destination confirmed — ${exchange.workspaceName} / ${exchange.productName}`);
39677
39734
  const apiKey = exchange.apiKey || current?.apiKey;
39678
39735
  if (!apiKey)
39679
39736
  throw new Error('No repository credential is available after adoption exchange.');
@@ -39686,7 +39743,10 @@ async function runAdopt(argv = process.argv) {
39686
39743
  // credential remains recoverable from this repo without minting a duplicate key.
39687
39744
  (0, project_config_1.saveProjectConnection)(config);
39688
39745
  (0, project_config_1.ensureProjectMcpConfig)();
39746
+ console.log(' ✓ Credential stored outside the repository');
39747
+ console.log(' ✓ Pathmode project entries added without the credential');
39689
39748
  const client = new api_client_1.PathmodeClient(config);
39749
+ console.log(' • Creating and linking the hosted intent…');
39690
39750
  const pushed = await (0, push_spec_1.pushSpec)({
39691
39751
  client,
39692
39752
  id: meta.id,
@@ -39717,11 +39777,17 @@ async function runAdopt(argv = process.argv) {
39717
39777
  if (!pushed.ok)
39718
39778
  throw new Error(pushed.error);
39719
39779
  await client.completeRepoOnboarding(exchange.sessionId, pushed.canonicalId);
39720
- console.log(' ');
39721
- console.log(` Adopted into ${exchange.workspaceName} / ${exchange.productName}`);
39780
+ console.log(' Hosted intent linked');
39722
39781
  console.log(` ${pushed.sourceUrl}`);
39723
39782
  console.log('');
39724
- console.log('Restart your coding tool once so it reloads the connected Pathmode server.');
39783
+ console.log('What changed');
39784
+ console.log(' • intent.md remains authoritative; only its cloud identity frontmatter changed');
39785
+ console.log(' • Project connection files contain no Pathmode credential');
39786
+ console.log(' • The hosted record is ready for human review and authorization');
39787
+ console.log('');
39788
+ console.log('Next');
39789
+ console.log(' Keep the browser page open; it finishes automatically.');
39790
+ console.log(' Restart your coding tool once so it reloads .mcp.json and discovers Pathmode.');
39725
39791
  console.log('');
39726
39792
  }
39727
39793
 
@@ -40428,6 +40494,9 @@ function getPriorityLabel(o) {
40428
40494
  return '';
40429
40495
  return `[${o.priority.toUpperCase()}] `;
40430
40496
  }
40497
+ function getOutcomeMeasurement(o) {
40498
+ return typeof o === 'string' ? '' : (0, serializeIntentMd_1.renderOutcomeMeasurementDefinition)(o.measurement);
40499
+ }
40431
40500
  function getLiveQualityCheckPromptBlock() {
40432
40501
  return `LIVE QUALITY CHECKS:
40433
40502
  - After EVERY meaningful user answer, silently update your current draft and run a quality pass before asking the next question.
@@ -40614,6 +40683,9 @@ function formatCursorRules(spec) {
40614
40683
  sections.push('Your implementation MUST satisfy ALL of these:');
40615
40684
  for (const outcome of spec.outcomes) {
40616
40685
  sections.push(`- ${getPriorityLabel(outcome)}${getOutcomeText(outcome)}`);
40686
+ const measurement = getOutcomeMeasurement(outcome);
40687
+ if (measurement)
40688
+ sections.push(` - measurement: ${measurement}`);
40617
40689
  }
40618
40690
  }
40619
40691
  if (spec.scope?.inScope?.length || spec.scope?.outOfScope?.length) {
@@ -40694,7 +40766,10 @@ function formatClaudeMdSection(spec) {
40694
40766
  sections.push(...decisionLines(spec.decisions, '**Decisions**:'));
40695
40767
  if (spec.outcomes?.length) {
40696
40768
  sections.push('**Outcomes**:');
40697
- sections.push(spec.outcomes.map(o => `- [ ] ${getPriorityLabel(o)}${getOutcomeText(o)}`).join('\n'));
40769
+ sections.push(spec.outcomes.map(o => {
40770
+ const measurement = getOutcomeMeasurement(o);
40771
+ return `- [ ] ${getPriorityLabel(o)}${getOutcomeText(o)}${measurement ? `\n - measurement: ${measurement}` : ''}`;
40772
+ }).join('\n'));
40698
40773
  }
40699
40774
  if (spec.scope?.inScope?.length || spec.scope?.outOfScope?.length) {
40700
40775
  const scopeParts = ['**Scope**:'];
@@ -40849,6 +40924,9 @@ function buildGraderRubric(spec, opts = {}) {
40849
40924
  for (const o of outcomes) {
40850
40925
  const bar = priorityToBar(getOutcomePriority(o));
40851
40926
  sections.push(`${i}. [${bar}] ${getOutcomeText(o)}`);
40927
+ const measurement = getOutcomeMeasurement(o);
40928
+ if (measurement)
40929
+ sections.push(` - Production signal: ${measurement}`);
40852
40930
  sections.push(' - Evidence: point to the specific value, state, or test result in the artifact that proves this. If you cannot, mark FAIL.');
40853
40931
  i++;
40854
40932
  }
@@ -41054,6 +41132,7 @@ exports.stripHtmlComments = stripHtmlComments;
41054
41132
  const fs_1 = __importDefault(__nccwpck_require__(9896));
41055
41133
  const path_1 = __importDefault(__nccwpck_require__(6928));
41056
41134
  const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
41135
+ const measurement_schema_1 = __nccwpck_require__(1635);
41057
41136
  const CONFIRMATION_HEADER_RE = /^\*\*(objective|outcomes)\*\*\s*[—–-]\s*(confirmed|waived)\s+by\s+(agent|human)\s*$/i;
41058
41137
  const CONFIRMATION_FIELD_RE = /^\s*[-*]\s+(actor|problem|outcome|observable|reason|anchor|at)\s*:\s*(.+)$/i;
41059
41138
  /**
@@ -41181,6 +41260,22 @@ function preferSection(fromSection, fromFrontmatter) {
41181
41260
  .map((v) => v.trim())
41182
41261
  .filter(Boolean);
41183
41262
  }
41263
+ function attachOutcomeMeasurements(outcomes, rawEntries) {
41264
+ if (!Array.isArray(rawEntries))
41265
+ return outcomes;
41266
+ const definitions = new Map();
41267
+ for (const raw of rawEntries) {
41268
+ const parsed = measurement_schema_1.outcomeMeasurementEntryShape.safeParse(raw);
41269
+ if (!parsed.success || parsed.data.outcome >= outcomes.length)
41270
+ continue;
41271
+ const { outcome, ...definition } = parsed.data;
41272
+ definitions.set(outcome, definition);
41273
+ }
41274
+ return outcomes.map((text, index) => {
41275
+ const measurement = definitions.get(index);
41276
+ return measurement ? { text, measurement } : text;
41277
+ });
41278
+ }
41184
41279
  /** Coerce a structured frontmatter entry ({text}, {description}) to its text, else ''. */
41185
41280
  function coerceItemText(v) {
41186
41281
  if (!v || typeof v !== 'object')
@@ -41233,7 +41328,7 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
41233
41328
  title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
41234
41329
  stageName: data.stage || undefined,
41235
41330
  severity: data.severity || undefined,
41236
- outcomes: preferSection(extractListSection(sections, 'Outcomes'), data.outcomes),
41331
+ outcomes: attachOutcomeMeasurements(preferSection(extractListSection(sections, 'Outcomes'), data.outcomes), data.outcomeMeasurements),
41237
41332
  decisions: extractDecisions(sections),
41238
41333
  constraints: preferSection(extractListSection(sections, 'Constraints'), data.constraints),
41239
41334
  edgeCases: (() => { const fromBody = extractEdgeCases(sections); return fromBody.length ? fromBody : frontmatterEdgeCases(data.edgeCases); })(),
@@ -41490,9 +41585,11 @@ function extractVerification(sections) {
41490
41585
  const result = {};
41491
41586
  let currentKind = null;
41492
41587
  let currentLegacy = null;
41588
+ let sawLabel = false;
41493
41589
  for (const line of lines) {
41494
41590
  const label = parseVerificationLabel(line);
41495
41591
  if (label) {
41592
+ sawLabel = true;
41496
41593
  const kind = VERIFICATION_LABEL_TO_KIND[label.toLowerCase()];
41497
41594
  if (kind) {
41498
41595
  currentKind = kind;
@@ -41531,6 +41628,11 @@ function extractVerification(sections) {
41531
41628
  else if (currentLegacy) {
41532
41629
  result[currentLegacy].push(text);
41533
41630
  }
41631
+ else if (!sawLabel) {
41632
+ // IntentSpec labels are optional. Treat an ungrouped list as manual checks so a valid
41633
+ // flat `## Verification` section survives adoption instead of silently disappearing.
41634
+ (result.manualChecks ||= []).push(text);
41635
+ }
41534
41636
  }
41535
41637
  return result;
41536
41638
  }
@@ -41565,7 +41667,7 @@ function hasVerificationContent(v) {
41565
41667
  * units and source labels.
41566
41668
  */
41567
41669
  Object.defineProperty(exports, "__esModule", ({ value: true }));
41568
- exports.recordOutcomeMeasurementInputSchema = exports.provenanceShape = exports.expectationShape = exports.windowShape = exports.queryRefShape = exports.windowKindEnum = exports.comparatorEnum = void 0;
41670
+ exports.recordOutcomeMeasurementInputSchema = exports.provenanceShape = exports.outcomeMeasurementEntryShape = exports.outcomeMeasurementDefinitionShape = exports.expectationShape = exports.windowShape = exports.queryRefShape = exports.windowKindEnum = exports.comparatorEnum = void 0;
41569
41671
  exports.outcomeSelectorError = outcomeSelectorError;
41570
41672
  const zod_1 = __nccwpck_require__(924);
41571
41673
  const MAX_TEXT = 500;
@@ -41602,6 +41704,19 @@ exports.expectationShape = zod_1.z.object({
41602
41704
  target: zod_1.z.number().finite(),
41603
41705
  unit: zod_1.z.string().trim().max(MAX_UNIT).optional(),
41604
41706
  });
41707
+ /** Mirrors the optional query recipe stored on a StructuredOutcome. */
41708
+ exports.outcomeMeasurementDefinitionShape = zod_1.z.object({
41709
+ source: zod_1.z.object({
41710
+ provider: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('System that owns the saved query, e.g. "posthog"'),
41711
+ queryRef: exports.queryRefShape,
41712
+ }),
41713
+ resultSelector: zod_1.z.string().trim().min(1).max(MAX_TEXT).optional().describe('Optional path into a compound provider response'),
41714
+ expectation: exports.expectationShape,
41715
+ window: exports.windowShape,
41716
+ });
41717
+ exports.outcomeMeasurementEntryShape = exports.outcomeMeasurementDefinitionShape.extend({
41718
+ outcome: zod_1.z.number().int().nonnegative(),
41719
+ });
41605
41720
  exports.provenanceShape = zod_1.z.object({
41606
41721
  provider: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('System the number came from, e.g. "posthog"'),
41607
41722
  queryRef: exports.queryRefShape,
@@ -42216,6 +42331,7 @@ exports.ensureProjectMcpConfig = ensureProjectMcpConfig;
42216
42331
  const fs_1 = __importDefault(__nccwpck_require__(9896));
42217
42332
  const os_1 = __importDefault(__nccwpck_require__(857));
42218
42333
  const path_1 = __importDefault(__nccwpck_require__(6928));
42334
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
42219
42335
  const homeWorkspaceFile = (workspaceId, homeDir = os_1.default.homedir()) => path_1.default.join(homeDir, '.pathmode', 'workspaces', `${workspaceId}.json`);
42220
42336
  function projectBindingFile(cwd = process.cwd()) {
42221
42337
  return path_1.default.join(cwd, '.pathmode', 'config.json');
@@ -42228,32 +42344,73 @@ function readJson(filePath) {
42228
42344
  return null;
42229
42345
  }
42230
42346
  }
42347
+ function normalizeApiUrl(raw) {
42348
+ if (typeof raw !== 'string' || !raw.trim())
42349
+ return null;
42350
+ try {
42351
+ const url = new URL(raw.trim());
42352
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
42353
+ return null;
42354
+ return url.origin;
42355
+ }
42356
+ catch {
42357
+ return null;
42358
+ }
42359
+ }
42360
+ /** Repository files are attacker-controlled when a project or branch is first opened. */
42361
+ function assertSafeProjectWrite(cwd, filePath) {
42362
+ const root = path_1.default.resolve(cwd);
42363
+ const target = path_1.default.resolve(filePath);
42364
+ const relative = path_1.default.relative(root, target);
42365
+ if (!relative || relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
42366
+ throw new Error(`Refusing to write outside the repository: ${filePath}`);
42367
+ }
42368
+ let current = root;
42369
+ for (const segment of relative.split(path_1.default.sep)) {
42370
+ current = path_1.default.join(current, segment);
42371
+ if (fs_1.default.existsSync(current) && fs_1.default.lstatSync(current).isSymbolicLink()) {
42372
+ throw new Error(`Refusing to write through a symbolic link: ${current}`);
42373
+ }
42374
+ }
42375
+ }
42376
+ function writeProjectFile(cwd, filePath, contents) {
42377
+ assertSafeProjectWrite(cwd, filePath);
42378
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
42379
+ // Re-check after creating a missing parent so a checked-in symlink is never followed.
42380
+ assertSafeProjectWrite(cwd, filePath);
42381
+ fs_1.default.writeFileSync(filePath, contents, 'utf8');
42382
+ }
42231
42383
  function loadProjectConnection(cwd = process.cwd(), homeDir = os_1.default.homedir()) {
42232
42384
  const binding = readJson(projectBindingFile(cwd));
42233
42385
  const workspaceId = typeof binding?.workspaceId === 'string' ? binding.workspaceId.trim() : '';
42234
- if (!workspaceId)
42386
+ if (!UUID_RE.test(workspaceId))
42235
42387
  return null;
42236
42388
  const secret = readJson(homeWorkspaceFile(workspaceId, homeDir));
42237
42389
  const apiKey = typeof secret?.apiKey === 'string' ? secret.apiKey.trim() : '';
42238
42390
  if (!apiKey)
42239
42391
  return null;
42240
- const apiUrl = typeof binding?.apiUrl === 'string' && binding.apiUrl.trim()
42241
- ? binding.apiUrl.trim()
42242
- : (typeof secret?.apiUrl === 'string' && secret.apiUrl.trim() ? secret.apiUrl.trim() : 'https://pathmode.io');
42392
+ // The repository chooses only which workspace to use. The protected home secret
42393
+ // chooses where that credential may be sent, so a malicious branch cannot redirect it.
42394
+ const apiUrl = normalizeApiUrl(secret?.apiUrl);
42395
+ if (!apiUrl)
42396
+ return null;
42243
42397
  return { apiKey, apiUrl, workspaceId };
42244
42398
  }
42245
42399
  function saveProjectConnection(config, cwd = process.cwd(), homeDir = os_1.default.homedir()) {
42400
+ if (!UUID_RE.test(config.workspaceId))
42401
+ throw new Error('Cannot save a project connection with an invalid workspace id.');
42402
+ const apiUrl = normalizeApiUrl(config.apiUrl);
42403
+ if (!apiUrl)
42404
+ throw new Error('Cannot save a project connection with an invalid API URL.');
42246
42405
  const bindingPath = projectBindingFile(cwd);
42247
- fs_1.default.mkdirSync(path_1.default.dirname(bindingPath), { recursive: true });
42248
- fs_1.default.writeFileSync(bindingPath, JSON.stringify({
42406
+ writeProjectFile(cwd, bindingPath, JSON.stringify({
42249
42407
  workspaceId: config.workspaceId,
42250
- apiUrl: config.apiUrl,
42251
- }, null, 2) + '\n', 'utf8');
42408
+ }, null, 2) + '\n');
42252
42409
  const secretPath = homeWorkspaceFile(config.workspaceId, homeDir);
42253
42410
  fs_1.default.mkdirSync(path_1.default.dirname(secretPath), { recursive: true });
42254
42411
  fs_1.default.writeFileSync(secretPath, JSON.stringify({
42255
42412
  apiKey: config.apiKey,
42256
- apiUrl: config.apiUrl,
42413
+ apiUrl,
42257
42414
  }, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
42258
42415
  try {
42259
42416
  fs_1.default.chmodSync(secretPath, 0o600);
@@ -42264,6 +42421,7 @@ function saveProjectConnection(config, cwd = process.cwd(), homeDir = os_1.defau
42264
42421
  * from the project binding at runtime and is never written into a committable file. */
42265
42422
  function ensureProjectMcpConfig(cwd = process.cwd()) {
42266
42423
  const configPath = path_1.default.join(cwd, '.mcp.json');
42424
+ assertSafeProjectWrite(cwd, configPath);
42267
42425
  const existing = fs_1.default.existsSync(configPath) ? readJson(configPath) : {};
42268
42426
  if (!existing)
42269
42427
  throw new Error(`${configPath} is not valid JSON; fix it before adopting.`);
@@ -42271,7 +42429,7 @@ function ensureProjectMcpConfig(cwd = process.cwd()) {
42271
42429
  ? existing.mcpServers
42272
42430
  : {};
42273
42431
  servers.pathmode = { command: 'npx', args: ['@pathmode/mcp-server'] };
42274
- fs_1.default.writeFileSync(configPath, JSON.stringify({ ...existing, mcpServers: servers }, null, 2) + '\n', 'utf8');
42432
+ writeProjectFile(cwd, configPath, JSON.stringify({ ...existing, mcpServers: servers }, null, 2) + '\n');
42275
42433
  }
42276
42434
 
42277
42435
 
@@ -50043,9 +50201,24 @@ exports.AjvJsonSchemaValidator = AjvJsonSchemaValidator;
50043
50201
  "use strict";
50044
50202
 
50045
50203
 
50046
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = __nccwpck_require__(5077)
50204
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = __nccwpck_require__(5077)
50047
50205
  const { SCHEMES, getSchemeHandler } = __nccwpck_require__(5300)
50048
50206
 
50207
+ const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
50208
+ const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.'
50209
+
50210
+ /**
50211
+ * @param {string} scheme
50212
+ * @returns {string}
50213
+ */
50214
+ function decodeValidScheme (scheme) {
50215
+ const decodedScheme = unescape(String(scheme))
50216
+ if (!VALID_SCHEME.test(decodedScheme)) {
50217
+ throw new TypeError(MALFORMED_SCHEME_ERROR)
50218
+ }
50219
+ return decodedScheme
50220
+ }
50221
+
50049
50222
  /**
50050
50223
  * @template {import('./types/index').URIComponent|string} T
50051
50224
  * @param {T} uri
@@ -50054,7 +50227,7 @@ const { SCHEMES, getSchemeHandler } = __nccwpck_require__(5300)
50054
50227
  */
50055
50228
  function normalize (uri, options) {
50056
50229
  if (typeof uri === 'string') {
50057
- uri = /** @type {T} */ (serialize(parse(uri, options), options))
50230
+ uri = /** @type {T} */ (normalizeString(uri, options))
50058
50231
  } else if (typeof uri === 'object') {
50059
50232
  uri = /** @type {T} */ (parse(serialize(uri, options), options))
50060
50233
  }
@@ -50069,7 +50242,50 @@ function normalize (uri, options) {
50069
50242
  */
50070
50243
  function resolve (baseURI, relativeURI, options) {
50071
50244
  const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
50072
- const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true)
50245
+ const {
50246
+ parsed: baseParsed,
50247
+ malformedAuthorityOrPort: baseMalformed,
50248
+ malformedPercentEncoding: baseMalformedPercentEncoding,
50249
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
50250
+ malformedHost: baseMalformedHost,
50251
+ malformedScheme: baseMalformedScheme
50252
+ } = parseWithStatus(baseURI, schemelessOptions)
50253
+ const {
50254
+ parsed: relativeParsed,
50255
+ malformedAuthorityOrPort: relativeMalformed,
50256
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
50257
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
50258
+ malformedHost: relativeMalformedHost,
50259
+ malformedScheme: relativeMalformedScheme
50260
+ } = parseWithStatus(relativeURI, schemelessOptions)
50261
+ if (
50262
+ baseMalformed ||
50263
+ relativeMalformed ||
50264
+ baseMalformedPercentEncoding ||
50265
+ relativeMalformedPercentEncoding ||
50266
+ baseMalformedSchemeSpecific ||
50267
+ relativeMalformedSchemeSpecific ||
50268
+ baseMalformedHost ||
50269
+ relativeMalformedHost ||
50270
+ baseMalformedScheme ||
50271
+ relativeMalformedScheme
50272
+ ) {
50273
+ throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
50274
+ }
50275
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
50276
+ const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
50277
+ const resolvedHost = resolved.host
50278
+ const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
50279
+ (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
50280
+ canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
50281
+ // Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
50282
+ // parser can reject them even though fast-uri preserves them safely as RFC
50283
+ // 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
50284
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
50285
+ !/\P{ASCII}/u.test(resolvedHost)
50286
+ if (resolved.error && !encodedASCIIHost) {
50287
+ throw new Error(resolved.error)
50288
+ }
50073
50289
  schemelessOptions.skipEscape = true
50074
50290
  return serialize(resolved, schemelessOptions)
50075
50291
  }
@@ -50149,21 +50365,10 @@ function resolveComponent (base, relative, options, skipNormalization) {
50149
50365
  * @returns {boolean}
50150
50366
  */
50151
50367
  function equal (uriA, uriB, options) {
50152
- if (typeof uriA === 'string') {
50153
- uriA = unescape(uriA)
50154
- uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
50155
- } else if (typeof uriA === 'object') {
50156
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
50157
- }
50368
+ const normalizedA = normalizeComparableURI(uriA, options)
50369
+ const normalizedB = normalizeComparableURI(uriB, options)
50158
50370
 
50159
- if (typeof uriB === 'string') {
50160
- uriB = unescape(uriB)
50161
- uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
50162
- } else if (typeof uriB === 'object') {
50163
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
50164
- }
50165
-
50166
- return uriA.toLowerCase() === uriB.toLowerCase()
50371
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
50167
50372
  }
50168
50373
 
50169
50374
  /**
@@ -50191,25 +50396,30 @@ function serialize (cmpts, opts) {
50191
50396
  const options = Object.assign({}, opts)
50192
50397
  const uriTokens = []
50193
50398
 
50399
+ if (component.scheme) {
50400
+ component.scheme = decodeValidScheme(component.scheme)
50401
+ }
50402
+
50194
50403
  // find scheme handler
50195
50404
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
50196
50405
 
50197
50406
  // perform scheme specific serialization
50198
50407
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
50199
50408
 
50409
+ const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
50410
+ const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
50411
+
50200
50412
  if (component.path !== undefined) {
50201
50413
  if (!options.skipEscape) {
50202
- component.path = escape(component.path)
50203
-
50204
- if (component.scheme !== undefined) {
50205
- component.path = component.path.split('%3A').join(':')
50206
- }
50414
+ component.path = serializePathEncoding(component.path, pathNoScheme)
50207
50415
  } else {
50208
- component.path = unescape(component.path)
50416
+ component.path = normalizePercentEncoding(component.path)
50209
50417
  }
50210
50418
  }
50211
50419
 
50212
50420
  if (options.reference !== 'suffix' && component.scheme) {
50421
+ // Scheme handlers may replace the scheme during serialization.
50422
+ component.scheme = decodeValidScheme(component.scheme)
50213
50423
  uriTokens.push(component.scheme, ':')
50214
50424
  }
50215
50425
 
@@ -50232,6 +50442,13 @@ function serialize (cmpts, opts) {
50232
50442
  s = removeDotSegments(s)
50233
50443
  }
50234
50444
 
50445
+ // Dot-segment removal can expose a colon that was not originally in the
50446
+ // first segment (for example, "./a:b"). Reapply path-noscheme encoding so
50447
+ // the serialized relative reference cannot be reparsed as a URI scheme.
50448
+ if (pathNoScheme) {
50449
+ s = serializePathEncoding(s, true)
50450
+ }
50451
+
50235
50452
  if (
50236
50453
  authority === undefined &&
50237
50454
  s[0] === '/' &&
@@ -50245,23 +50462,117 @@ function serialize (cmpts, opts) {
50245
50462
  }
50246
50463
 
50247
50464
  if (component.query !== undefined) {
50248
- uriTokens.push('?', component.query)
50465
+ uriTokens.push('?', encodeQuery(component.query))
50249
50466
  }
50250
50467
 
50251
50468
  if (component.fragment !== undefined) {
50252
- uriTokens.push('#', component.fragment)
50469
+ uriTokens.push('#', encodeFragment(component.fragment))
50253
50470
  }
50254
50471
  return uriTokens.join('')
50255
50472
  }
50256
50473
 
50257
50474
  const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
50258
50475
 
50476
+ // Captures the authority component (between "//" and the next "/", "?" or "#"),
50477
+ // with or without a scheme prefix, for the literal-backslash rejection below.
50478
+ const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
50479
+
50480
+ // Captures the leading authority-introducer region after an optional scheme: a
50481
+ // run of forward slashes, backslashes, and the characters the WHATWG URL parser
50482
+ // removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
50483
+ // is exactly "//". Node treats "\" as "/" on special schemes and strips those
50484
+ // characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
50485
+ // "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
50486
+ // the path group (host confusion / SSRF / redirect bypass).
50487
+ const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
50488
+
50489
+ /**
50490
+ * @param {import('./types/index').URIComponent} parsed
50491
+ * @param {RegExpMatchArray} matches
50492
+ * @returns {string|undefined}
50493
+ */
50494
+ function getParseError (parsed, matches) {
50495
+ if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
50496
+ return 'URI path must start with "/" when authority is present.'
50497
+ }
50498
+
50499
+ if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
50500
+ return 'URI port is malformed.'
50501
+ }
50502
+
50503
+ return undefined
50504
+ }
50505
+
50506
+ /**
50507
+ * Checks percent syntax without decoding the represented octets. RFC 3986
50508
+ * percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
50509
+ * though they are not independently valid UTF-8.
50510
+ *
50511
+ * @param {string|undefined} component
50512
+ * @returns {boolean}
50513
+ */
50514
+ function hasMalformedPercentEncoding (component) {
50515
+ if (component === undefined) return false
50516
+
50517
+ let percent = component.indexOf('%')
50518
+ while (percent !== -1) {
50519
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
50520
+ return true
50521
+ }
50522
+ percent = component.indexOf('%', percent + 3)
50523
+ }
50524
+
50525
+ return false
50526
+ }
50527
+
50528
+ /**
50529
+ * @param {RegExpMatchArray} matches
50530
+ * @returns {boolean}
50531
+ */
50532
+ function hasMalformedComponentPercentEncoding (matches) {
50533
+ // Bracketed IP literals use a raw "%" as the zone separator for historical
50534
+ // compatibility. Their parsing is intentionally left to normalizeIPv6.
50535
+ const host = matches[4]
50536
+ return hasMalformedPercentEncoding(matches[3]) ||
50537
+ (host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
50538
+ hasMalformedPercentEncoding(matches[6]) ||
50539
+ hasMalformedPercentEncoding(matches[7]) ||
50540
+ hasMalformedPercentEncoding(matches[8])
50541
+ }
50542
+
50543
+ /**
50544
+ * @param {import('./types/index').URIComponent} parsed
50545
+ * @param {import('./types/index').Options} options
50546
+ * @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
50547
+ * @param {boolean} isIP
50548
+ * @returns {boolean} whether host conversion failed
50549
+ */
50550
+ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
50551
+ if (
50552
+ !options.unicodeSupport &&
50553
+ (!schemeHandler || !schemeHandler.unicodeSupport) &&
50554
+ parsed.host &&
50555
+ parsed.host[0] !== '[' &&
50556
+ (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
50557
+ isIP === false &&
50558
+ nonSimpleDomain(parsed.host)
50559
+ ) {
50560
+ try {
50561
+ parsed.host = new URL('http://' + parsed.host).hostname
50562
+ } catch (e) {
50563
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
50564
+ return true
50565
+ }
50566
+ }
50567
+ return false
50568
+ }
50569
+
50259
50570
  /**
50260
50571
  * @param {string} uri
50261
50572
  * @param {import('./types/index').Options} [opts]
50262
- * @returns
50573
+ * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
50263
50574
  */
50264
- function parse (uri, opts) {
50575
+ function parseWithStatus (uri, opts) {
50265
50576
  const options = Object.assign({}, opts)
50266
50577
  /** @type {import('./types/index').URIComponent} */
50267
50578
  const parsed = {
@@ -50274,6 +50585,13 @@ function parse (uri, opts) {
50274
50585
  fragment: undefined
50275
50586
  }
50276
50587
 
50588
+ let malformedAuthorityOrPort = false
50589
+ let malformedPercentEncoding = false
50590
+ let malformedSchemeSpecific = false
50591
+ let malformedHost = false
50592
+ let malformedIPLiteral = false
50593
+ let malformedScheme = false
50594
+
50277
50595
  let isIP = false
50278
50596
  if (options.reference === 'suffix') {
50279
50597
  if (options.scheme) {
@@ -50283,6 +50601,41 @@ function parse (uri, opts) {
50283
50601
  }
50284
50602
  }
50285
50603
 
50604
+ // A literal backslash (U+005C) is not a valid RFC 3986 URI character and is
50605
+ // not an authority delimiter. Reject it in the authority rather than
50606
+ // rewriting it: normalizing "\" -> "/" (WHATWG error recovery) could silently
50607
+ // change the resource identified by an otherwise-invalid input, and lets "\"
50608
+ // act as a host delimiter here while Node's native URL parses a different
50609
+ // host (SSRF / redirect / origin-allowlist bypass). Percent-encoded %5C is
50610
+ // untouched and remains valid encoded data.
50611
+ const authorityMatch = uri.match(AUTHORITY_PREFIX)
50612
+ if (authorityMatch !== null && authorityMatch[1].indexOf('\\') !== -1) {
50613
+ parsed.error = 'URI authority must not contain a literal backslash.'
50614
+ malformedAuthorityOrPort = true
50615
+ }
50616
+
50617
+ // Reject a malformed or whitespace-smuggled authority introducer. fast-uri
50618
+ // only recognizes a literal "//"; anything else in the leading separator run
50619
+ // (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
50620
+ // Node strips) means the authority fast-uri parses differs from the one Node's
50621
+ // URL resolves. Reject rather than rewrite, mirroring the literal-backslash
50622
+ // guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
50623
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
50624
+ if (introducerMatch !== null) {
50625
+ const region = introducerMatch[1]
50626
+ const normalizedRegion = region.replace(/[\t\n\r]/g, '')
50627
+ // Two or more leading separators introduce an authority.
50628
+ if (normalizedRegion.length >= 2) {
50629
+ if (normalizedRegion.slice(0, 2) !== '//') {
50630
+ parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
50631
+ malformedAuthorityOrPort = true
50632
+ } else if (region.length !== normalizedRegion.length) {
50633
+ parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
50634
+ malformedAuthorityOrPort = true
50635
+ }
50636
+ }
50637
+ }
50638
+
50286
50639
  const matches = uri.match(URI_PARSE)
50287
50640
 
50288
50641
  if (matches) {
@@ -50295,16 +50648,45 @@ function parse (uri, opts) {
50295
50648
  parsed.query = matches[7]
50296
50649
  parsed.fragment = matches[8]
50297
50650
 
50651
+ if (parsed.scheme !== undefined) {
50652
+ const decodedScheme = unescape(parsed.scheme)
50653
+ if (VALID_SCHEME.test(decodedScheme)) {
50654
+ parsed.scheme = decodedScheme.toLowerCase()
50655
+ } else {
50656
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
50657
+ malformedScheme = true
50658
+ }
50659
+ }
50660
+
50661
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
50662
+ if (malformedPercentEncoding) {
50663
+ parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
50664
+ }
50665
+
50298
50666
  // fix port number
50299
50667
  if (isNaN(parsed.port)) {
50300
50668
  parsed.port = matches[5]
50301
50669
  }
50670
+
50671
+ const parseError = getParseError(parsed, matches)
50672
+ if (parseError !== undefined) {
50673
+ parsed.error = parsed.error || parseError
50674
+ malformedAuthorityOrPort = true
50675
+ }
50676
+
50302
50677
  if (parsed.host) {
50303
50678
  const ipv4result = isIPv4(parsed.host)
50304
50679
  if (ipv4result === false) {
50680
+ const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
50305
50681
  const ipv6result = normalizeIPv6(parsed.host)
50306
- parsed.host = ipv6result.host.toLowerCase()
50307
- isIP = ipv6result.isIPV6
50682
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
50683
+ malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
50684
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
50685
+
50686
+ if (malformedIPLiteral) {
50687
+ parsed.error = parsed.error || 'URI host is malformed.'
50688
+ malformedAuthorityOrPort = true
50689
+ }
50308
50690
  } else {
50309
50691
  isIP = true
50310
50692
  }
@@ -50327,45 +50709,93 @@ function parse (uri, opts) {
50327
50709
  // find scheme handler
50328
50710
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
50329
50711
 
50330
- // check if scheme can't handle IRIs
50331
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
50332
- // if host component is a domain name
50333
- if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
50334
- // convert Unicode IDN -> ASCII IDN
50335
- try {
50336
- parsed.host = URL.domainToASCII(parsed.host.toLowerCase())
50337
- } catch (e) {
50338
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
50339
- }
50340
- }
50341
- // convert IRI -> URI
50342
- }
50712
+ // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
50713
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
50343
50714
 
50344
50715
  if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
50345
50716
  if (uri.indexOf('%') !== -1) {
50346
- if (parsed.scheme !== undefined) {
50347
- parsed.scheme = unescape(parsed.scheme)
50348
- }
50349
- if (parsed.host !== undefined) {
50350
- parsed.host = unescape(parsed.host)
50717
+ if (parsed.host !== undefined && !malformedIPLiteral) {
50718
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
50719
+ parsed.host = reescapeHostDelimiters(host, isIP)
50351
50720
  }
50352
50721
  }
50353
50722
  if (parsed.path) {
50354
- parsed.path = escape(unescape(parsed.path))
50723
+ parsed.path = normalizePathEncoding(parsed.path)
50724
+ }
50725
+ if (parsed.query) {
50726
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query)
50355
50727
  }
50356
50728
  if (parsed.fragment) {
50357
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
50729
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
50358
50730
  }
50359
50731
  }
50360
50732
 
50361
50733
  // perform scheme specific parsing
50362
50734
  if (schemeHandler && schemeHandler.parse) {
50363
50735
  schemeHandler.parse(parsed, options)
50736
+ if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
50737
+ malformedSchemeSpecific = true
50738
+ }
50364
50739
  }
50365
50740
  } else {
50366
50741
  parsed.error = parsed.error || 'URI can not be parsed.'
50367
50742
  }
50368
- return parsed
50743
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
50744
+ }
50745
+
50746
+ /**
50747
+ * @param {string} uri
50748
+ * @param {import('./types/index').Options} [opts]
50749
+ * @returns
50750
+ */
50751
+ function parse (uri, opts) {
50752
+ return parseWithStatus(uri, opts).parsed
50753
+ }
50754
+
50755
+ /**
50756
+ * @param {string} uri
50757
+ * @param {import('./types/index').Options} [opts]
50758
+ * @returns {string}
50759
+ */
50760
+ function normalizeString (uri, opts) {
50761
+ return normalizeStringWithStatus(uri, opts).normalized
50762
+ }
50763
+
50764
+ /**
50765
+ * @param {string} uri
50766
+ * @param {import('./types/index').Options} [opts]
50767
+ * @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
50768
+ */
50769
+ function normalizeStringWithStatus (uri, opts) {
50770
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
50771
+ return {
50772
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
50773
+ malformedAuthorityOrPort,
50774
+ malformedPercentEncoding,
50775
+ malformedSchemeSpecific,
50776
+ malformedHost,
50777
+ malformedScheme
50778
+ }
50779
+ }
50780
+
50781
+ /**
50782
+ * @param {import ('./types/index').URIComponent|string} uri
50783
+ * @param {import('./types/index').Options} [opts]
50784
+ * @returns {string|undefined}
50785
+ */
50786
+ function normalizeComparableURI (uri, opts) {
50787
+ if (typeof uri !== 'string' && typeof uri !== 'object') {
50788
+ return undefined
50789
+ }
50790
+
50791
+ let value
50792
+ try {
50793
+ value = typeof uri === 'string' ? uri : serialize(uri, opts)
50794
+ } catch {
50795
+ return undefined
50796
+ }
50797
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
50798
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
50369
50799
  }
50370
50800
 
50371
50801
  const fastUri = {
@@ -50392,7 +50822,7 @@ module.exports.fastUri = fastUri
50392
50822
 
50393
50823
 
50394
50824
  const { isUUID } = __nccwpck_require__(5077)
50395
- const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu
50825
+ const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu
50396
50826
 
50397
50827
  const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
50398
50828
  'wss', 'urn', 'urn:uuid'])
@@ -50504,9 +50934,14 @@ function wsSerialize (wsComponent) {
50504
50934
 
50505
50935
  // reconstruct path from resource name
50506
50936
  if (wsComponent.resourceName) {
50507
- const [path, query] = wsComponent.resourceName.split('?')
50937
+ const queryIndex = wsComponent.resourceName.indexOf('?')
50938
+ const path = queryIndex === -1
50939
+ ? wsComponent.resourceName
50940
+ : wsComponent.resourceName.slice(0, queryIndex)
50508
50941
  wsComponent.path = (path && path !== '/' ? path : undefined)
50509
- wsComponent.query = query
50942
+ wsComponent.query = queryIndex === -1
50943
+ ? undefined
50944
+ : wsComponent.resourceName.slice(queryIndex + 1)
50510
50945
  wsComponent.resourceName = undefined
50511
50946
  }
50512
50947
 
@@ -50523,7 +50958,7 @@ function urnParse (urnComponent, options) {
50523
50958
  return urnComponent
50524
50959
  }
50525
50960
  const matches = urnComponent.path.match(URN_REG)
50526
- if (matches) {
50961
+ if (matches && matches[0] === urnComponent.path) {
50527
50962
  const scheme = options.scheme || urnComponent.scheme || 'urn'
50528
50963
  urnComponent.nid = matches[1].toLowerCase()
50529
50964
  urnComponent.nss = matches[2]
@@ -50672,6 +51107,44 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
50672
51107
  /** @type {(value: string) => boolean} */
50673
51108
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
50674
51109
 
51110
+ /** @type {(value: string) => boolean} */
51111
+ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
51112
+
51113
+ /** @type {(value: string) => boolean} */
51114
+ const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
51115
+
51116
+ /** @type {(value: string) => boolean} */
51117
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
51118
+
51119
+ /** @type {(value: string) => boolean} */
51120
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
51121
+
51122
+ /** @type {(value: string) => boolean} */
51123
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
51124
+
51125
+ const BYTE_HEX = new Array(256)
51126
+ {
51127
+ const HEX_DIGITS = '0123456789ABCDEF'
51128
+ for (let i = 0; i < 256; i++) {
51129
+ BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
51130
+ }
51131
+ }
51132
+ function percentEncodeNonAscii (cp) {
51133
+ if (cp < 0x800) {
51134
+ return BYTE_HEX[0xC0 | (cp >> 6)] +
51135
+ BYTE_HEX[0x80 | (cp & 0x3F)]
51136
+ }
51137
+ if (cp < 0x10000) {
51138
+ return BYTE_HEX[0xE0 | (cp >> 12)] +
51139
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
51140
+ BYTE_HEX[0x80 | (cp & 0x3F)]
51141
+ }
51142
+ return BYTE_HEX[0xF0 | (cp >> 18)] +
51143
+ BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
51144
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
51145
+ BYTE_HEX[0x80 | (cp & 0x3F)]
51146
+ }
51147
+
50675
51148
  /**
50676
51149
  * @param {Array<string>} input
50677
51150
  * @returns {string}
@@ -50703,12 +51176,14 @@ function stringArrayToHexStripped (input) {
50703
51176
  return acc
50704
51177
  }
50705
51178
 
50706
- /**
50707
- * @typedef {Object} GetIPV6Result
50708
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
50709
- * @property {string} address - The parsed IPv6 address.
50710
- * @property {string} [zone] - The zone identifier, if present.
50711
- */
51179
+ /** @type {(value: string) => boolean} */
51180
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
51181
+
51182
+ /** @type {(value: string) => boolean} */
51183
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
51184
+
51185
+ /** @type {(value: string) => boolean} */
51186
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
50712
51187
 
50713
51188
  /**
50714
51189
  * @param {string} value
@@ -50717,88 +51192,104 @@ function stringArrayToHexStripped (input) {
50717
51192
  const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
50718
51193
 
50719
51194
  /**
50720
- * @param {Array<string>} buffer
51195
+ * @param {string} zone
50721
51196
  * @returns {boolean}
50722
51197
  */
50723
- function consumeIsZone (buffer) {
50724
- buffer.length = 0
51198
+ function isZoneIdentifier (zone) {
51199
+ if (zone.length === 0) return false
51200
+
51201
+ for (let i = 0; i < zone.length; i++) {
51202
+ if (isZoneCharacter(zone[i])) continue
51203
+ if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
51204
+ i += 2
51205
+ continue
51206
+ }
51207
+ return false
51208
+ }
51209
+
50725
51210
  return true
50726
51211
  }
50727
51212
 
50728
51213
  /**
50729
- * @param {Array<string>} buffer
50730
- * @param {Array<string>} address
50731
- * @param {GetIPV6Result} output
50732
- * @returns {boolean}
51214
+ * Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
51215
+ * single zero hextet is left uncompressed. On ties the leftmost run wins.
51216
+ *
51217
+ * @param {string[]} hextets
51218
+ * @returns {string}
50733
51219
  */
50734
- function consumeHextets (buffer, address, output) {
50735
- if (buffer.length) {
50736
- const hex = stringArrayToHexStripped(buffer)
50737
- if (hex !== '') {
50738
- address.push(hex)
51220
+ function compressIPv6ZeroRun (hextets) {
51221
+ let bestStart = -1
51222
+ let bestLength = 0
51223
+ let runStart = -1
51224
+ let runLength = 0
51225
+ for (let i = 0; i < hextets.length; i++) {
51226
+ if (hextets[i] === '0') {
51227
+ if (runStart === -1) runStart = i
51228
+ runLength++
51229
+ if (runLength > bestLength) {
51230
+ bestLength = runLength
51231
+ bestStart = runStart
51232
+ }
50739
51233
  } else {
50740
- output.error = true
50741
- return false
51234
+ runStart = -1
51235
+ runLength = 0
50742
51236
  }
50743
- buffer.length = 0
50744
51237
  }
50745
- return true
51238
+
51239
+ if (bestLength < 2) return hextets.join(':')
51240
+
51241
+ const head = hextets.slice(0, bestStart).join(':')
51242
+ const tail = hextets.slice(bestStart + bestLength).join(':')
51243
+ return head + '::' + tail
50746
51244
  }
50747
51245
 
50748
51246
  /**
51247
+ * Validates an IPv6 address against the alternatives in RFC 3986 section
51248
+ * 3.2.2 and returns the same address with leading hextet zeroes removed.
51249
+ * An embedded IPv4 address counts as two hextets and is only valid at the end.
51250
+ *
50749
51251
  * @param {string} input
50750
- * @returns {GetIPV6Result}
51252
+ * @returns {string|undefined}
50751
51253
  */
50752
- function getIPV6 (input) {
50753
- let tokenCount = 0
50754
- const output = { error: false, address: '', zone: '' }
50755
- /** @type {Array<string>} */
50756
- const address = []
50757
- /** @type {Array<string>} */
50758
- const buffer = []
50759
- let endipv6Encountered = false
50760
- let endIpv6 = false
50761
-
50762
- let consume = consumeHextets
50763
-
50764
- for (let i = 0; i < input.length; i++) {
50765
- const cursor = input[i]
50766
- if (cursor === '[' || cursor === ']') { continue }
50767
- if (cursor === ':') {
50768
- if (endipv6Encountered === true) {
50769
- endIpv6 = true
50770
- }
50771
- if (!consume(buffer, address, output)) { break }
50772
- if (++tokenCount > 7) {
50773
- // not valid
50774
- output.error = true
50775
- break
50776
- }
50777
- if (i > 0 && input[i - 1] === ':') {
50778
- endipv6Encountered = true
50779
- }
50780
- address.push(':')
50781
- continue
50782
- } else if (cursor === '%') {
50783
- if (!consume(buffer, address, output)) { break }
50784
- // switch to zone detection
50785
- consume = consumeIsZone
50786
- } else {
50787
- buffer.push(cursor)
51254
+ function normalizeIPv6Address (input) {
51255
+ const compression = input.indexOf('::')
51256
+ if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
51257
+
51258
+ const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
51259
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
51260
+ if (compression !== -1) {
51261
+ if (left.length === 1 && left[0] === '') left.length = 0
51262
+ if (right.length === 1 && right[0] === '') right.length = 0
51263
+ }
51264
+
51265
+ const parts = left.concat(right)
51266
+ let hextetCount = 0
51267
+ for (let i = 0; i < parts.length; i++) {
51268
+ const part = parts[i]
51269
+ if (part === '') return undefined
51270
+
51271
+ if (part.indexOf('.') !== -1) {
51272
+ if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
51273
+ hextetCount += 2
50788
51274
  continue
50789
51275
  }
51276
+
51277
+ if (!isHextet(part)) return undefined
51278
+ parts[i] = parseInt(part, 16).toString(16)
51279
+ hextetCount++
50790
51280
  }
50791
- if (buffer.length) {
50792
- if (consume === consumeIsZone) {
50793
- output.zone = buffer.join('')
50794
- } else if (endIpv6) {
50795
- address.push(buffer.join(''))
50796
- } else {
50797
- address.push(stringArrayToHexStripped(buffer))
50798
- }
51281
+
51282
+ if (compression === -1) {
51283
+ if (hextetCount !== 8) return undefined
51284
+ return compressIPv6ZeroRun(parts)
50799
51285
  }
50800
- output.address = address.join('')
50801
- return output
51286
+ if (hextetCount >= 8) return undefined
51287
+
51288
+ // expand "::" then re-compress the longest run for a canonical result
51289
+ const expanded = parts.slice(0, left.length)
51290
+ for (let i = hextetCount; i < 8; i++) expanded.push('0')
51291
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
51292
+ return compressIPv6ZeroRun(expanded)
50802
51293
  }
50803
51294
 
50804
51295
  /**
@@ -50806,26 +51297,49 @@ function getIPV6 (input) {
50806
51297
  * @property {string} host - The normalized host.
50807
51298
  * @property {string} [escapedHost] - The escaped host.
50808
51299
  * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
51300
+ * @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
51301
+ * @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
50809
51302
  */
50810
51303
 
50811
51304
  /**
51305
+ * Validates and normalizes a bracketed IP literal. Raw zone separators remain
51306
+ * accepted for backwards compatibility, while encoded separators and zone
51307
+ * contents follow RFC 6874.
51308
+ *
50812
51309
  * @param {string} host
50813
51310
  * @returns {NormalizeIPv6Result}
50814
51311
  */
50815
51312
  function normalizeIPv6 (host) {
50816
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
50817
- const ipv6 = getIPV6(host)
51313
+ const bracketed = host[0] === '[' && host[host.length - 1] === ']'
51314
+ const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
51315
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
50818
51316
 
50819
- if (!ipv6.error) {
50820
- let newHost = ipv6.address
50821
- let escapedHost = ipv6.address
50822
- if (ipv6.zone) {
50823
- newHost += '%' + ipv6.zone
50824
- escapedHost += '%25' + ipv6.zone
50825
- }
50826
- return { host: newHost, isIPV6: true, escapedHost }
50827
- } else {
50828
- return { host, isIPV6: false }
51317
+ let input = bracketed ? host.slice(1, -1) : host
51318
+ if (bracketed && isIPvFuture(input)) {
51319
+ input = input.toLowerCase()
51320
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
51321
+ }
51322
+
51323
+ if (findToken(input, ':') < 2) {
51324
+ return { host, isIPV6: false, error: bracketed }
51325
+ }
51326
+
51327
+ let zoneIdentifier = ''
51328
+ const zoneSeparator = input.indexOf('%')
51329
+ if (zoneSeparator !== -1) {
51330
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
51331
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength)
51332
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
51333
+ input = input.slice(0, zoneSeparator)
51334
+ }
51335
+
51336
+ const address = normalizeIPv6Address(input)
51337
+ if (address === undefined) return { host, isIPV6: false, error: true }
51338
+
51339
+ return {
51340
+ host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
51341
+ escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
51342
+ isIPV6: true
50829
51343
  }
50830
51344
  }
50831
51345
 
@@ -50930,31 +51444,342 @@ function removeDotSegments (path) {
50930
51444
  }
50931
51445
 
50932
51446
  /**
50933
- * @param {import('../types/index').URIComponent} component
50934
- * @param {boolean} esc
50935
- * @returns {import('../types/index').URIComponent}
51447
+ * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
51448
+ * After the URI regex parses, these characters cannot be literal in the host
51449
+ * field, so any that appear after decoding came from percent-encoding and
51450
+ * must be restored to prevent authority structure changes.
51451
+ *
51452
+ * @param {string} host
51453
+ * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
51454
+ * @returns {string}
50936
51455
  */
50937
- function normalizeComponentEncoding (component, esc) {
50938
- const func = esc !== true ? escape : unescape
50939
- if (component.scheme !== undefined) {
50940
- component.scheme = func(component.scheme)
51456
+ const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
51457
+ const HOST_DELIM_RE = /[@/?#:]/g
51458
+ const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
51459
+
51460
+ function reescapeHostDelimiters (host, isIP) {
51461
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
51462
+ re.lastIndex = 0
51463
+ return host.replace(re, (ch) => HOST_DELIMS[ch])
51464
+ }
51465
+
51466
+ /**
51467
+ * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
51468
+ * Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
51469
+ *
51470
+ * @param {string} input
51471
+ * @param {boolean} [decodeUnreserved=false]
51472
+ * @returns {string}
51473
+ */
51474
+ function normalizePercentEncoding (input, decodeUnreserved = false) {
51475
+ if (input.indexOf('%') === -1) {
51476
+ return input
50941
51477
  }
50942
- if (component.userinfo !== undefined) {
50943
- component.userinfo = func(component.userinfo)
51478
+
51479
+ let output = ''
51480
+
51481
+ for (let i = 0; i < input.length; i++) {
51482
+ if (input[i] === '%' && i + 2 < input.length) {
51483
+ const hex = input.slice(i + 1, i + 3)
51484
+ if (isHexPair(hex)) {
51485
+ const normalizedHex = hex.toUpperCase()
51486
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
51487
+
51488
+ if (decodeUnreserved && isUnreserved(decoded)) {
51489
+ output += decoded
51490
+ } else {
51491
+ output += '%' + normalizedHex
51492
+ }
51493
+
51494
+ i += 2
51495
+ continue
51496
+ }
51497
+ }
51498
+
51499
+ output += input[i]
50944
51500
  }
50945
- if (component.host !== undefined) {
50946
- component.host = func(component.host)
51501
+
51502
+ return output
51503
+ }
51504
+
51505
+ /**
51506
+ * Normalizes path data without turning reserved escapes into live path syntax.
51507
+ * Valid escapes are uppercased, raw unsafe characters are escaped, and only
51508
+ * unreserved bytes that are not `.` are decoded.
51509
+ *
51510
+ * @param {string} input
51511
+ * @returns {string}
51512
+ */
51513
+ function normalizePathEncoding (input) {
51514
+ let output = ''
51515
+
51516
+ for (let i = 0; i < input.length; i++) {
51517
+ const ch = input[i]
51518
+ if (ch === '%' && i + 2 < input.length) {
51519
+ const hex = input.slice(i + 1, i + 3)
51520
+ if (isHexPair(hex)) {
51521
+ const normalizedHex = hex.toUpperCase()
51522
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
51523
+
51524
+ if (decoded !== '.' && isUnreserved(decoded)) {
51525
+ output += decoded
51526
+ } else {
51527
+ output += '%' + normalizedHex
51528
+ }
51529
+
51530
+ i += 2
51531
+ continue
51532
+ }
51533
+ }
51534
+
51535
+ if (isPathCharacter(ch)) {
51536
+ output += ch
51537
+ } else {
51538
+ const code = input.charCodeAt(i)
51539
+ if (code < 0x80) {
51540
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
51541
+ } else if (code < 0xD800 || code > 0xDFFF) {
51542
+ output += percentEncodeNonAscii(code)
51543
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
51544
+ const low = input.charCodeAt(i + 1)
51545
+ if (low >= 0xDC00 && low <= 0xDFFF) {
51546
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
51547
+ i++
51548
+ } else {
51549
+ output += percentEncodeNonAscii(0xFFFD)
51550
+ }
51551
+ } else {
51552
+ output += percentEncodeNonAscii(0xFFFD)
51553
+ }
51554
+ }
50947
51555
  }
50948
- if (component.path !== undefined) {
50949
- component.path = func(component.path)
51556
+
51557
+ return output
51558
+ }
51559
+
51560
+ /**
51561
+ * Serializes a path without rewriting reserved data. Raw RFC 3986 path
51562
+ * characters remain literal, valid escapes are preserved and uppercased, and
51563
+ * everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
51564
+ * first segment must be escaped so the result cannot be parsed as a scheme.
51565
+ *
51566
+ * @param {string} input
51567
+ * @param {boolean} [pathNoScheme=false]
51568
+ * @returns {string}
51569
+ */
51570
+ function serializePathEncoding (input, pathNoScheme = false) {
51571
+ let output = ''
51572
+ let firstSegment = pathNoScheme && input[0] !== '/'
51573
+
51574
+ for (let i = 0; i < input.length; i++) {
51575
+ const ch = input[i]
51576
+ if (ch === '%' && i + 2 < input.length) {
51577
+ const hex = input.slice(i + 1, i + 3)
51578
+ if (isHexPair(hex)) {
51579
+ output += '%' + hex.toUpperCase()
51580
+ i += 2
51581
+ continue
51582
+ }
51583
+ }
51584
+
51585
+ if (ch === '/') {
51586
+ firstSegment = false
51587
+ }
51588
+
51589
+ if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
51590
+ output += ch
51591
+ } else {
51592
+ const code = input.charCodeAt(i)
51593
+ if (code < 0x80) {
51594
+ output += BYTE_HEX[code]
51595
+ } else if (code < 0xD800 || code > 0xDFFF) {
51596
+ output += percentEncodeNonAscii(code)
51597
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
51598
+ const low = input.charCodeAt(i + 1)
51599
+ if (low >= 0xDC00 && low <= 0xDFFF) {
51600
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
51601
+ i++
51602
+ } else {
51603
+ output += percentEncodeNonAscii(0xFFFD)
51604
+ }
51605
+ } else {
51606
+ output += percentEncodeNonAscii(0xFFFD)
51607
+ }
51608
+ }
50950
51609
  }
50951
- if (component.query !== undefined) {
50952
- component.query = func(component.query)
51610
+
51611
+ return output
51612
+ }
51613
+
51614
+ /**
51615
+ * Percent-encodes a URI component using its RFC 3986 literal character set.
51616
+ * Existing valid escapes are preserved and normalized to uppercase hex.
51617
+ *
51618
+ * @param {string} input
51619
+ * @param {(value: string) => boolean} isAllowed
51620
+ * @returns {string}
51621
+ */
51622
+ function encodeComponent (input, isAllowed) {
51623
+ let output = ''
51624
+
51625
+ for (let i = 0; i < input.length; i++) {
51626
+ const ch = input[i]
51627
+ if (ch === '%' && i + 2 < input.length) {
51628
+ const hex = input.slice(i + 1, i + 3)
51629
+ if (isHexPair(hex)) {
51630
+ output += '%' + hex.toUpperCase()
51631
+ i += 2
51632
+ continue
51633
+ }
51634
+ }
51635
+
51636
+ if (isAllowed(ch)) {
51637
+ output += ch
51638
+ } else {
51639
+ const code = input.charCodeAt(i)
51640
+ if (code < 0x80) {
51641
+ output += BYTE_HEX[code]
51642
+ } else if (code < 0xD800 || code > 0xDFFF) {
51643
+ output += percentEncodeNonAscii(code)
51644
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
51645
+ const low = input.charCodeAt(i + 1)
51646
+ if (low >= 0xDC00 && low <= 0xDFFF) {
51647
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
51648
+ i++
51649
+ } else {
51650
+ output += percentEncodeNonAscii(0xFFFD)
51651
+ }
51652
+ } else {
51653
+ output += percentEncodeNonAscii(0xFFFD)
51654
+ }
51655
+ }
50953
51656
  }
50954
- if (component.fragment !== undefined) {
50955
- component.fragment = func(component.fragment)
51657
+
51658
+ return output
51659
+ }
51660
+
51661
+ /**
51662
+ * Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
51663
+ * In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
51664
+ *
51665
+ * @param {string} input
51666
+ * @returns {string}
51667
+ */
51668
+ function encodeUserinfo (input) {
51669
+ return encodeComponent(input, isUserinfoCharacter)
51670
+ }
51671
+
51672
+ /**
51673
+ * Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
51674
+ * escaped because it would otherwise begin the fragment component.
51675
+ *
51676
+ * @param {string} input
51677
+ * @returns {string}
51678
+ */
51679
+ function encodeQuery (input) {
51680
+ return encodeComponent(input, isQueryFragmentCharacter)
51681
+ }
51682
+
51683
+ /**
51684
+ * Encodes fragment data using the RFC 3986 §3.5 grammar.
51685
+ *
51686
+ * @param {string} input
51687
+ * @returns {string}
51688
+ */
51689
+ function encodeFragment (input) {
51690
+ return encodeComponent(input, isQueryFragmentCharacter)
51691
+ }
51692
+
51693
+ function isEscapeSafe (cp) {
51694
+ return (
51695
+ (cp >= 0x30 && cp <= 0x39) ||
51696
+ (cp >= 0x41 && cp <= 0x5A) ||
51697
+ (cp >= 0x61 && cp <= 0x7A) ||
51698
+ cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
51699
+ cp === 0x2F || cp === 0x40 || cp === 0x5F
51700
+ )
51701
+ }
51702
+
51703
+ /**
51704
+ * Normalizes the percent-encoding of a query or fragment component.
51705
+ *
51706
+ * Like `normalizePathEncoding`, but uses the query/fragment character set
51707
+ * (which additionally allows `?`) and decodes `.` since it has no dot-segment
51708
+ * meaning outside of a path.
51709
+ *
51710
+ * @param {string} input
51711
+ * @returns {string}
51712
+ */
51713
+ function normalizeQueryFragmentEncoding (input) {
51714
+ let output = ''
51715
+
51716
+ for (let i = 0; i < input.length; i++) {
51717
+ const ch = input[i]
51718
+ if (ch === '%' && i + 2 < input.length) {
51719
+ const hex = input.slice(i + 1, i + 3)
51720
+ if (isHexPair(hex)) {
51721
+ const normalizedHex = hex.toUpperCase()
51722
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
51723
+
51724
+ if (isUnreserved(decoded)) {
51725
+ output += decoded
51726
+ } else {
51727
+ output += '%' + normalizedHex
51728
+ }
51729
+
51730
+ i += 2
51731
+ continue
51732
+ }
51733
+ }
51734
+
51735
+ if (isQueryFragmentCharacter(ch)) {
51736
+ output += ch
51737
+ } else {
51738
+ const code = input.charCodeAt(i)
51739
+ if (code < 0x80) {
51740
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
51741
+ } else if (code < 0xD800 || code > 0xDFFF) {
51742
+ output += percentEncodeNonAscii(code)
51743
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
51744
+ const low = input.charCodeAt(i + 1)
51745
+ if (low >= 0xDC00 && low <= 0xDFFF) {
51746
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
51747
+ i++
51748
+ } else {
51749
+ output += percentEncodeNonAscii(0xFFFD)
51750
+ }
51751
+ } else {
51752
+ output += percentEncodeNonAscii(0xFFFD)
51753
+ }
51754
+ }
50956
51755
  }
50957
- return component
51756
+
51757
+ return output
51758
+ }
51759
+
51760
+ /**
51761
+ * Escapes a component while preserving existing valid percent escapes.
51762
+ *
51763
+ * @param {string} input
51764
+ * @returns {string}
51765
+ */
51766
+ function escapePreservingEscapes (input) {
51767
+ let output = ''
51768
+
51769
+ for (let i = 0; i < input.length; i++) {
51770
+ if (input[i] === '%' && i + 2 < input.length) {
51771
+ const hex = input.slice(i + 1, i + 3)
51772
+ if (isHexPair(hex)) {
51773
+ output += '%' + hex.toUpperCase()
51774
+ i += 2
51775
+ continue
51776
+ }
51777
+ }
51778
+
51779
+ output += escape(input[i])
51780
+ }
51781
+
51782
+ return output
50958
51783
  }
50959
51784
 
50960
51785
  /**
@@ -50965,18 +51790,24 @@ function recomposeAuthority (component) {
50965
51790
  const uriTokens = []
50966
51791
 
50967
51792
  if (component.userinfo !== undefined) {
50968
- uriTokens.push(component.userinfo)
51793
+ uriTokens.push(encodeUserinfo(component.userinfo))
50969
51794
  uriTokens.push('@')
50970
51795
  }
50971
51796
 
50972
51797
  if (component.host !== undefined) {
50973
- let host = unescape(component.host)
51798
+ let host = component.host
50974
51799
  if (!isIPv4(host)) {
50975
- const ipV6res = normalizeIPv6(host)
50976
- if (ipV6res.isIPV6 === true) {
51800
+ let ipV6res = normalizeIPv6(host)
51801
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
51802
+ // Decode only unreserved bytes, once. In particular, keep %25 encoded
51803
+ // so it cannot introduce a second escape during recomposition.
51804
+ host = normalizePercentEncoding(host, true)
51805
+ ipV6res = normalizeIPv6(host)
51806
+ }
51807
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
50977
51808
  host = `[${ipV6res.escapedHost}]`
50978
51809
  } else {
50979
- host = component.host
51810
+ host = reescapeHostDelimiters(host, false)
50980
51811
  }
50981
51812
  }
50982
51813
  uriTokens.push(host)
@@ -50993,7 +51824,15 @@ function recomposeAuthority (component) {
50993
51824
  module.exports = {
50994
51825
  nonSimpleDomain,
50995
51826
  recomposeAuthority,
50996
- normalizeComponentEncoding,
51827
+ reescapeHostDelimiters,
51828
+ normalizePercentEncoding,
51829
+ normalizePathEncoding,
51830
+ serializePathEncoding,
51831
+ normalizeQueryFragmentEncoding,
51832
+ encodeUserinfo,
51833
+ encodeQuery,
51834
+ encodeFragment,
51835
+ escapePreservingEscapes,
50997
51836
  removeDotSegments,
50998
51837
  isIPv4,
50999
51838
  isUUID,
@@ -71946,7 +72785,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
71946
72785
  /***/ ((module) => {
71947
72786
 
71948
72787
  "use strict";
71949
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.20.0","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Deterministic intent preflight before your agent builds: six calibrated gates, keyless, no model call. Draft and sharpen specs in conversation, or connect a Pathmode workspace to sync intent and evidence across a team.","main":"dist/index.js","bin":{"pathmode-mcp":"dist/index.js"},"files":["dist/","manifest.json","icon.svg","README.md","skills/"],"scripts":{"build":"rm -rf dist && ncc build src/index.ts -o dist","dev":"ts-node src/index.ts","prepublishOnly":"npm run build"},"keywords":["pathmode","mcp","model-context-protocol","claude-code","claude-code-skills","agent-skills","cursor","windsurf","intent-engineering","intent-compiler","ai-agents","product-development","dependency-graph","strategic-planning"],"author":"Pathmode","license":"MIT","type":"commonjs","engines":{"node":">=18.0.0"},"homepage":"https://pathmode.io","dependencies":{"@modelcontextprotocol/sdk":"^1.12.1","gray-matter":"^4.0.3","zod":"^3.24.0"},"devDependencies":{"@types/node":"^25.1.0","@vercel/ncc":"^0.38.4","ts-node":"^10.9.2","typescript":"^5.9.3"}}');
72788
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.20.2","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Deterministic intent preflight before your agent builds: six calibrated gates, keyless, no model call. Draft and sharpen specs in conversation, or connect a Pathmode workspace to sync intent and evidence across a team.","main":"dist/index.js","bin":{"pathmode-mcp":"dist/index.js"},"files":["dist/","manifest.json","icon.svg","README.md","skills/"],"scripts":{"build":"rm -rf dist && ncc build src/index.ts -o dist","dev":"ts-node src/index.ts","prepublishOnly":"npm run build"},"keywords":["pathmode","mcp","model-context-protocol","claude-code","claude-code-skills","agent-skills","cursor","windsurf","intent-engineering","intent-compiler","ai-agents","product-development","dependency-graph","strategic-planning"],"author":"Pathmode","license":"MIT","type":"commonjs","engines":{"node":">=18.0.0"},"homepage":"https://pathmode.io","dependencies":{"@modelcontextprotocol/sdk":"^1.12.1","gray-matter":"^4.0.3","zod":"^3.24.0"},"overrides":{"@hono/node-server":"^1.19.17","body-parser":"^2.3.0","fast-uri":"^3.1.6","hono":"^4.13.5","ip-address":"^10.7.0","js-yaml":"^3.15.2"},"devDependencies":{"@types/node":"^25.1.0","@vercel/ncc":"^0.38.4","ts-node":"^10.9.2","typescript":"^5.9.3"}}');
71950
72789
 
71951
72790
  /***/ })
71952
72791
 
@@ -72406,6 +73245,16 @@ function startMcpServer() {
72406
73245
  return { content: [{ type: 'text', text: `Failed to fetch intent: ${e.message}` }] };
72407
73246
  }
72408
73247
  });
73248
+ const outcomeInputShape = zod_1.z.union([
73249
+ zod_1.z.string().trim().min(1),
73250
+ zod_1.z.object({
73251
+ id: zod_1.z.string().trim().min(1).optional(),
73252
+ text: zod_1.z.string().trim().min(1),
73253
+ priority: zod_1.z.enum(['must', 'should', 'could']).optional(),
73254
+ /** Null is meaningful on update: deliberately remove the stored recipe. */
73255
+ measurement: measurement_schema_1.outcomeMeasurementDefinitionShape.nullable().optional(),
73256
+ }),
73257
+ ]);
72409
73258
  cloudOnly.registerTool('get_intent_relations', {
72410
73259
  title: 'Get Intent Relations',
72411
73260
  description: 'Get the dependency graph for a specific intent. Shows what it depends on, enables, or blocks.',
@@ -72974,7 +73823,7 @@ function startMcpServer() {
72974
73823
  objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
72975
73824
  currentState: zod_1.z.string().optional().describe('How this works today, before the change: existing behavior, the workaround users rely on, what is broken. Tells implementing agents what must not regress. Omit for genuinely net-new capability.'),
72976
73825
  productId: zod_1.z.string().optional().describe('Product (Space) ID this intent belongs to. Omit it and the server resolves it when the workspace has exactly one real product. If it cannot, you get code PRODUCT_REQUIRED with the candidate list: ASK THE USER which product, then retry with an explicit id. Do not guess — a GitHub binding hangs off the product, so the product decides which repository the PR stamp and merge verification apply to, and a wrong guess surfaces much later as a merge that silently fails to verify.'),
72977
- outcomes: zod_1.z.array(zod_1.z.string()).optional().describe('Observable, testable state changes'),
73826
+ outcomes: zod_1.z.array(outcomeInputShape).optional().describe('Observable, testable state changes. Use the structured form to attach a stable saved-query recipe; for PostHog, source.queryRef is typically kind "saved_insight" plus the insight id.'),
72978
73827
  constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Hard limits the implementation must respect'),
72979
73828
  healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('What to monitor after shipping'),
72980
73829
  edgeCases: zod_1.z.array(zod_1.z.object({
@@ -73015,7 +73864,7 @@ function startMcpServer() {
73015
73864
  title: zod_1.z.string().optional().describe('New title'),
73016
73865
  objective: zod_1.z.string().optional().describe('Updated objective'),
73017
73866
  currentState: zod_1.z.string().optional().describe('Updated as-is behavior (how it works today). Pass an empty string to clear it.'),
73018
- outcomes: zod_1.z.array(zod_1.z.string()).optional().describe('Replace all outcomes'),
73867
+ outcomes: zod_1.z.array(outcomeInputShape).optional().describe('Replace all outcomes. Omit measurement on an outcome to preserve its stored recipe; send measurement: null to clear it.'),
73019
73868
  constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Replace all constraints'),
73020
73869
  healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace all health metrics'),
73021
73870
  edgeCases: zod_1.z.array(zod_1.z.object({
@@ -73246,7 +74095,7 @@ function startMcpServer() {
73246
74095
  currentState: zod_1.z.string().optional().describe('How this works today, before the change — existing behavior and workarounds, so the implementation knows what must not regress. Omit for net-new capability.'),
73247
74096
  implementationContext: zod_1.z.string().optional().describe('What the repo actually looks like where this change lands: the files and modules involved, what already exists, what it would touch, how to verify it. You are in the working tree — go read it. Markdown, sub-headings welcome. Advisory: it never changes the readiness verdict, it just stops the implementing agent from rediscovering the codebase.'),
73248
74097
  productId: zod_1.z.string().optional().describe('Which product this intent belongs to (cloud mode only). Omit it and the server resolves it when the workspace has exactly one real product; if it cannot, the save fails with PRODUCT_REQUIRED and the candidate list, and you should ask the user which one and retry with an explicit id rather than guessing — a GitHub binding hangs off the product, so it decides which repository merge verification applies to.'),
73249
- outcomes: zod_1.z.array(zod_1.z.string()).describe('Observable, testable state changes'),
74098
+ outcomes: zod_1.z.array(outcomeInputShape).describe('Observable, testable state changes. The structured form may carry an optional provider-neutral saved-query measurement recipe.'),
73250
74099
  decisions: zod_1.z.array(zod_1.z.object({
73251
74100
  choice: zod_1.z.string(),
73252
74101
  ruledOut: zod_1.z.string().optional(),