@pathmode/mcp-server 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/index.js +1192 -185
- package/dist/packages/intentspec-format/serializeIntentMd.d.ts +24 -0
- package/dist/packages/intentspec-format/serializeIntentMd.d.ts.map +1 -1
- package/dist/packages/mcp-server/src/api-client.d.ts +68 -0
- package/dist/packages/mcp-server/src/api-client.d.ts.map +1 -1
- package/dist/packages/mcp-server/src/intent-compiler.d.ts +2 -0
- package/dist/packages/mcp-server/src/intent-compiler.d.ts.map +1 -1
- package/dist/packages/mcp-server/src/local-reader.d.ts +5 -1
- package/dist/packages/mcp-server/src/local-reader.d.ts.map +1 -1
- package/dist/packages/mcp-server/src/measurement-schema.d.ts +257 -18
- package/dist/packages/mcp-server/src/measurement-schema.d.ts.map +1 -1
- package/dist/packages/mcp-server/src/push-spec.d.ts +11 -0
- package/dist/packages/mcp-server/src/push-spec.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +9 -1
- package/skills/preflight/SKILL.md +3 -1
- package/skills/review-against-intent/SKILL.md +3 -1
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 =
|
|
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 (
|
|
38526
|
-
|
|
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', '
|
|
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 =>
|
|
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
|
|
@@ -40054,6 +40103,25 @@ class PathmodeClient {
|
|
|
40054
40103
|
});
|
|
40055
40104
|
return res.json();
|
|
40056
40105
|
}
|
|
40106
|
+
async listIntentChangeRequests(intentId, status = 'open') {
|
|
40107
|
+
const res = await this.fetch(`/intents/${intentId}/change-requests?status=${encodeURIComponent(status)}`);
|
|
40108
|
+
return res.json();
|
|
40109
|
+
}
|
|
40110
|
+
async listWorkspaceIntentChangeRequests(status = 'open') {
|
|
40111
|
+
const res = await this.fetch(`/change-requests?status=${encodeURIComponent(status)}`);
|
|
40112
|
+
return res.json();
|
|
40113
|
+
}
|
|
40114
|
+
async getIntentChangeRequest(intentId, requestId) {
|
|
40115
|
+
const res = await this.fetch(`/intents/${intentId}/change-requests/${requestId}`);
|
|
40116
|
+
return res.json();
|
|
40117
|
+
}
|
|
40118
|
+
async rejectIntentChangeRequest(intentId, requestId, reason) {
|
|
40119
|
+
const res = await this.fetch(`/intents/${intentId}/change-requests/${requestId}`, {
|
|
40120
|
+
method: 'PATCH',
|
|
40121
|
+
body: JSON.stringify({ action: 'reject', reason }),
|
|
40122
|
+
});
|
|
40123
|
+
return res.json();
|
|
40124
|
+
}
|
|
40057
40125
|
async queryEvidence(filters = {}) {
|
|
40058
40126
|
const params = new URLSearchParams();
|
|
40059
40127
|
for (const [key, val] of Object.entries(filters)) {
|
|
@@ -40445,6 +40513,9 @@ function getPriorityLabel(o) {
|
|
|
40445
40513
|
return '';
|
|
40446
40514
|
return `[${o.priority.toUpperCase()}] `;
|
|
40447
40515
|
}
|
|
40516
|
+
function getOutcomeMeasurement(o) {
|
|
40517
|
+
return typeof o === 'string' ? '' : (0, serializeIntentMd_1.renderOutcomeMeasurementDefinition)(o.measurement);
|
|
40518
|
+
}
|
|
40448
40519
|
function getLiveQualityCheckPromptBlock() {
|
|
40449
40520
|
return `LIVE QUALITY CHECKS:
|
|
40450
40521
|
- After EVERY meaningful user answer, silently update your current draft and run a quality pass before asking the next question.
|
|
@@ -40631,6 +40702,9 @@ function formatCursorRules(spec) {
|
|
|
40631
40702
|
sections.push('Your implementation MUST satisfy ALL of these:');
|
|
40632
40703
|
for (const outcome of spec.outcomes) {
|
|
40633
40704
|
sections.push(`- ${getPriorityLabel(outcome)}${getOutcomeText(outcome)}`);
|
|
40705
|
+
const measurement = getOutcomeMeasurement(outcome);
|
|
40706
|
+
if (measurement)
|
|
40707
|
+
sections.push(` - measurement: ${measurement}`);
|
|
40634
40708
|
}
|
|
40635
40709
|
}
|
|
40636
40710
|
if (spec.scope?.inScope?.length || spec.scope?.outOfScope?.length) {
|
|
@@ -40711,7 +40785,10 @@ function formatClaudeMdSection(spec) {
|
|
|
40711
40785
|
sections.push(...decisionLines(spec.decisions, '**Decisions**:'));
|
|
40712
40786
|
if (spec.outcomes?.length) {
|
|
40713
40787
|
sections.push('**Outcomes**:');
|
|
40714
|
-
sections.push(spec.outcomes.map(o =>
|
|
40788
|
+
sections.push(spec.outcomes.map(o => {
|
|
40789
|
+
const measurement = getOutcomeMeasurement(o);
|
|
40790
|
+
return `- [ ] ${getPriorityLabel(o)}${getOutcomeText(o)}${measurement ? `\n - measurement: ${measurement}` : ''}`;
|
|
40791
|
+
}).join('\n'));
|
|
40715
40792
|
}
|
|
40716
40793
|
if (spec.scope?.inScope?.length || spec.scope?.outOfScope?.length) {
|
|
40717
40794
|
const scopeParts = ['**Scope**:'];
|
|
@@ -40866,6 +40943,9 @@ function buildGraderRubric(spec, opts = {}) {
|
|
|
40866
40943
|
for (const o of outcomes) {
|
|
40867
40944
|
const bar = priorityToBar(getOutcomePriority(o));
|
|
40868
40945
|
sections.push(`${i}. [${bar}] ${getOutcomeText(o)}`);
|
|
40946
|
+
const measurement = getOutcomeMeasurement(o);
|
|
40947
|
+
if (measurement)
|
|
40948
|
+
sections.push(` - Production signal: ${measurement}`);
|
|
40869
40949
|
sections.push(' - Evidence: point to the specific value, state, or test result in the artifact that proves this. If you cannot, mark FAIL.');
|
|
40870
40950
|
i++;
|
|
40871
40951
|
}
|
|
@@ -41071,6 +41151,7 @@ exports.stripHtmlComments = stripHtmlComments;
|
|
|
41071
41151
|
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
41072
41152
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
41073
41153
|
const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
|
|
41154
|
+
const measurement_schema_1 = __nccwpck_require__(1635);
|
|
41074
41155
|
const CONFIRMATION_HEADER_RE = /^\*\*(objective|outcomes)\*\*\s*[—–-]\s*(confirmed|waived)\s+by\s+(agent|human)\s*$/i;
|
|
41075
41156
|
const CONFIRMATION_FIELD_RE = /^\s*[-*]\s+(actor|problem|outcome|observable|reason|anchor|at)\s*:\s*(.+)$/i;
|
|
41076
41157
|
/**
|
|
@@ -41198,6 +41279,22 @@ function preferSection(fromSection, fromFrontmatter) {
|
|
|
41198
41279
|
.map((v) => v.trim())
|
|
41199
41280
|
.filter(Boolean);
|
|
41200
41281
|
}
|
|
41282
|
+
function attachOutcomeMeasurements(outcomes, rawEntries) {
|
|
41283
|
+
if (!Array.isArray(rawEntries))
|
|
41284
|
+
return outcomes;
|
|
41285
|
+
const definitions = new Map();
|
|
41286
|
+
for (const raw of rawEntries) {
|
|
41287
|
+
const parsed = measurement_schema_1.outcomeMeasurementEntryShape.safeParse(raw);
|
|
41288
|
+
if (!parsed.success || parsed.data.outcome >= outcomes.length)
|
|
41289
|
+
continue;
|
|
41290
|
+
const { outcome, ...definition } = parsed.data;
|
|
41291
|
+
definitions.set(outcome, definition);
|
|
41292
|
+
}
|
|
41293
|
+
return outcomes.map((text, index) => {
|
|
41294
|
+
const measurement = definitions.get(index);
|
|
41295
|
+
return measurement ? { text, measurement } : text;
|
|
41296
|
+
});
|
|
41297
|
+
}
|
|
41201
41298
|
/** Coerce a structured frontmatter entry ({text}, {description}) to its text, else ''. */
|
|
41202
41299
|
function coerceItemText(v) {
|
|
41203
41300
|
if (!v || typeof v !== 'object')
|
|
@@ -41250,7 +41347,7 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
|
|
|
41250
41347
|
title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
|
|
41251
41348
|
stageName: data.stage || undefined,
|
|
41252
41349
|
severity: data.severity || undefined,
|
|
41253
|
-
outcomes: preferSection(extractListSection(sections, 'Outcomes'), data.outcomes),
|
|
41350
|
+
outcomes: attachOutcomeMeasurements(preferSection(extractListSection(sections, 'Outcomes'), data.outcomes), data.outcomeMeasurements),
|
|
41254
41351
|
decisions: extractDecisions(sections),
|
|
41255
41352
|
constraints: preferSection(extractListSection(sections, 'Constraints'), data.constraints),
|
|
41256
41353
|
edgeCases: (() => { const fromBody = extractEdgeCases(sections); return fromBody.length ? fromBody : frontmatterEdgeCases(data.edgeCases); })(),
|
|
@@ -41589,7 +41686,7 @@ function hasVerificationContent(v) {
|
|
|
41589
41686
|
* units and source labels.
|
|
41590
41687
|
*/
|
|
41591
41688
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
41592
|
-
exports.recordOutcomeMeasurementInputSchema = exports.provenanceShape = exports.expectationShape = exports.windowShape = exports.queryRefShape = exports.windowKindEnum = exports.comparatorEnum = void 0;
|
|
41689
|
+
exports.recordOutcomeMeasurementInputSchema = exports.provenanceShape = exports.outcomeMeasurementEntryShape = exports.outcomeMeasurementDefinitionShape = exports.expectationShape = exports.windowShape = exports.queryRefShape = exports.windowKindEnum = exports.comparatorEnum = void 0;
|
|
41593
41690
|
exports.outcomeSelectorError = outcomeSelectorError;
|
|
41594
41691
|
const zod_1 = __nccwpck_require__(924);
|
|
41595
41692
|
const MAX_TEXT = 500;
|
|
@@ -41626,6 +41723,19 @@ exports.expectationShape = zod_1.z.object({
|
|
|
41626
41723
|
target: zod_1.z.number().finite(),
|
|
41627
41724
|
unit: zod_1.z.string().trim().max(MAX_UNIT).optional(),
|
|
41628
41725
|
});
|
|
41726
|
+
/** Mirrors the optional query recipe stored on a StructuredOutcome. */
|
|
41727
|
+
exports.outcomeMeasurementDefinitionShape = zod_1.z.object({
|
|
41728
|
+
source: zod_1.z.object({
|
|
41729
|
+
provider: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('System that owns the saved query, e.g. "posthog"'),
|
|
41730
|
+
queryRef: exports.queryRefShape,
|
|
41731
|
+
}),
|
|
41732
|
+
resultSelector: zod_1.z.string().trim().min(1).max(MAX_TEXT).optional().describe('Optional path into a compound provider response'),
|
|
41733
|
+
expectation: exports.expectationShape,
|
|
41734
|
+
window: exports.windowShape,
|
|
41735
|
+
});
|
|
41736
|
+
exports.outcomeMeasurementEntryShape = exports.outcomeMeasurementDefinitionShape.extend({
|
|
41737
|
+
outcome: zod_1.z.number().int().nonnegative(),
|
|
41738
|
+
});
|
|
41629
41739
|
exports.provenanceShape = zod_1.z.object({
|
|
41630
41740
|
provider: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('System the number came from, e.g. "posthog"'),
|
|
41631
41741
|
queryRef: exports.queryRefShape,
|
|
@@ -42377,6 +42487,11 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
42377
42487
|
exports.pushSpec = pushSpec;
|
|
42378
42488
|
async function pushSpec(input) {
|
|
42379
42489
|
const { client, id, existingSpecVersion, payload } = input;
|
|
42490
|
+
// Confirmations are cloud-authored judgment. They are deliberately absent from the
|
|
42491
|
+
// intent_save tool shape, and this last boundary keeps a future caller from accidentally
|
|
42492
|
+
// turning a repo push into a replace-all confirmation import.
|
|
42493
|
+
const identityPayload = { ...payload };
|
|
42494
|
+
delete identityPayload.confirmations;
|
|
42380
42495
|
let canonicalId;
|
|
42381
42496
|
let specVersion;
|
|
42382
42497
|
let sourceUrl;
|
|
@@ -42389,11 +42504,18 @@ async function pushSpec(input) {
|
|
|
42389
42504
|
// quietly break the merge resolution that stamp exists for.
|
|
42390
42505
|
let saved;
|
|
42391
42506
|
if (existingSpecVersion) {
|
|
42392
|
-
saved = await client.updateIntent(id, {
|
|
42507
|
+
saved = await client.updateIntent(id, {
|
|
42508
|
+
...identityPayload,
|
|
42509
|
+
expectedVersion: existingSpecVersion,
|
|
42510
|
+
...(input.changeRequest ? {
|
|
42511
|
+
changeRequestId: input.changeRequest.id,
|
|
42512
|
+
expectedRepoBodyRevision: input.changeRequest.baseRepoBodyRevision,
|
|
42513
|
+
} : {}),
|
|
42514
|
+
});
|
|
42393
42515
|
}
|
|
42394
42516
|
else {
|
|
42395
42517
|
try {
|
|
42396
|
-
saved = await client.createIntent({ ...
|
|
42518
|
+
saved = await client.createIntent({ ...identityPayload, id });
|
|
42397
42519
|
}
|
|
42398
42520
|
catch (e) {
|
|
42399
42521
|
// The create may have COMMITTED and lost its response — a dropped connection after
|
|
@@ -42428,12 +42550,44 @@ async function pushSpec(input) {
|
|
|
42428
42550
|
}
|
|
42429
42551
|
input.onIdentitySettled({ canonicalId, specVersion, sourceUrl, origin, authorization, authorizationNote });
|
|
42430
42552
|
const didNotTravel = [];
|
|
42553
|
+
let pendingChangeRequest = input.changeRequest;
|
|
42554
|
+
if (pendingChangeRequest && input.client.getIntentChangeRequest) {
|
|
42555
|
+
try {
|
|
42556
|
+
const settled = await input.client.getIntentChangeRequest(canonicalId, pendingChangeRequest.id);
|
|
42557
|
+
if (settled.changeRequest?.status !== 'open')
|
|
42558
|
+
pendingChangeRequest = undefined;
|
|
42559
|
+
}
|
|
42560
|
+
catch {
|
|
42561
|
+
// Keep the original context. The write endpoint still performs the authoritative lock
|
|
42562
|
+
// and stale-base check; a failed observation must not silently drop that guard.
|
|
42563
|
+
}
|
|
42564
|
+
}
|
|
42431
42565
|
for (const d of input.decisions ?? []) {
|
|
42432
42566
|
try {
|
|
42433
|
-
await client.recordDecision(canonicalId, {
|
|
42567
|
+
await client.recordDecision(canonicalId, {
|
|
42568
|
+
choice: d.choice,
|
|
42569
|
+
reason: d.reason,
|
|
42570
|
+
ruledOut: d.ruledOut,
|
|
42571
|
+
...(pendingChangeRequest ? {
|
|
42572
|
+
changeRequestId: pendingChangeRequest.id,
|
|
42573
|
+
expectedRepoBodyRevision: pendingChangeRequest.baseRepoBodyRevision,
|
|
42574
|
+
} : {}),
|
|
42575
|
+
});
|
|
42434
42576
|
}
|
|
42435
42577
|
catch {
|
|
42436
42578
|
didNotTravel.push(`decision "${d.choice.slice(0, 40)}" — save again to retry`);
|
|
42579
|
+
continue;
|
|
42580
|
+
}
|
|
42581
|
+
if (pendingChangeRequest && input.client.getIntentChangeRequest) {
|
|
42582
|
+
try {
|
|
42583
|
+
const settled = await input.client.getIntentChangeRequest(canonicalId, pendingChangeRequest.id);
|
|
42584
|
+
if (settled.changeRequest?.status !== 'open')
|
|
42585
|
+
pendingChangeRequest = undefined;
|
|
42586
|
+
}
|
|
42587
|
+
catch {
|
|
42588
|
+
// The decision already persisted. A failed observation cannot turn that successful
|
|
42589
|
+
// write into a false enrichment failure; the final status read reports uncertainty.
|
|
42590
|
+
}
|
|
42437
42591
|
}
|
|
42438
42592
|
}
|
|
42439
42593
|
if (input.implementationContext?.trim()) {
|
|
@@ -42443,12 +42597,30 @@ async function pushSpec(input) {
|
|
|
42443
42597
|
await client.saveImplementationContext(canonicalId, {
|
|
42444
42598
|
currentBehavior: input.implementationContext,
|
|
42445
42599
|
...(specVersion ? { expectedSpecVersion: specVersion } : {}),
|
|
42600
|
+
...(pendingChangeRequest ? {
|
|
42601
|
+
changeRequestId: pendingChangeRequest.id,
|
|
42602
|
+
expectedRepoBodyRevision: pendingChangeRequest.baseRepoBodyRevision,
|
|
42603
|
+
} : {}),
|
|
42446
42604
|
});
|
|
42447
42605
|
}
|
|
42448
42606
|
catch {
|
|
42449
42607
|
didNotTravel.push('implementation context — call record_implementation_context to retry');
|
|
42450
42608
|
}
|
|
42451
42609
|
}
|
|
42610
|
+
if (pendingChangeRequest && input.client.getIntentChangeRequest) {
|
|
42611
|
+
try {
|
|
42612
|
+
const finalRequest = await input.client.getIntentChangeRequest(canonicalId, pendingChangeRequest.id);
|
|
42613
|
+
if (finalRequest.changeRequest?.status === 'open') {
|
|
42614
|
+
didNotTravel.push(`change request ${pendingChangeRequest.id} remains open — the synced revision did not satisfy it`);
|
|
42615
|
+
}
|
|
42616
|
+
else if (finalRequest.changeRequest?.status === 'superseded') {
|
|
42617
|
+
didNotTravel.push(`change request ${pendingChangeRequest.id} was superseded — re-read it before making another change`);
|
|
42618
|
+
}
|
|
42619
|
+
}
|
|
42620
|
+
catch {
|
|
42621
|
+
didNotTravel.push(`change request ${pendingChangeRequest.id} status could not be confirmed`);
|
|
42622
|
+
}
|
|
42623
|
+
}
|
|
42452
42624
|
return { ok: true, canonicalId, specVersion, sourceUrl, didNotTravel };
|
|
42453
42625
|
}
|
|
42454
42626
|
|
|
@@ -50110,9 +50282,24 @@ exports.AjvJsonSchemaValidator = AjvJsonSchemaValidator;
|
|
|
50110
50282
|
"use strict";
|
|
50111
50283
|
|
|
50112
50284
|
|
|
50113
|
-
const { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
50285
|
+
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = __nccwpck_require__(5077)
|
|
50114
50286
|
const { SCHEMES, getSchemeHandler } = __nccwpck_require__(5300)
|
|
50115
50287
|
|
|
50288
|
+
const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
|
|
50289
|
+
const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.'
|
|
50290
|
+
|
|
50291
|
+
/**
|
|
50292
|
+
* @param {string} scheme
|
|
50293
|
+
* @returns {string}
|
|
50294
|
+
*/
|
|
50295
|
+
function decodeValidScheme (scheme) {
|
|
50296
|
+
const decodedScheme = unescape(String(scheme))
|
|
50297
|
+
if (!VALID_SCHEME.test(decodedScheme)) {
|
|
50298
|
+
throw new TypeError(MALFORMED_SCHEME_ERROR)
|
|
50299
|
+
}
|
|
50300
|
+
return decodedScheme
|
|
50301
|
+
}
|
|
50302
|
+
|
|
50116
50303
|
/**
|
|
50117
50304
|
* @template {import('./types/index').URIComponent|string} T
|
|
50118
50305
|
* @param {T} uri
|
|
@@ -50121,7 +50308,7 @@ const { SCHEMES, getSchemeHandler } = __nccwpck_require__(5300)
|
|
|
50121
50308
|
*/
|
|
50122
50309
|
function normalize (uri, options) {
|
|
50123
50310
|
if (typeof uri === 'string') {
|
|
50124
|
-
uri = /** @type {T} */ (
|
|
50311
|
+
uri = /** @type {T} */ (normalizeString(uri, options))
|
|
50125
50312
|
} else if (typeof uri === 'object') {
|
|
50126
50313
|
uri = /** @type {T} */ (parse(serialize(uri, options), options))
|
|
50127
50314
|
}
|
|
@@ -50136,7 +50323,50 @@ function normalize (uri, options) {
|
|
|
50136
50323
|
*/
|
|
50137
50324
|
function resolve (baseURI, relativeURI, options) {
|
|
50138
50325
|
const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
|
|
50139
|
-
const
|
|
50326
|
+
const {
|
|
50327
|
+
parsed: baseParsed,
|
|
50328
|
+
malformedAuthorityOrPort: baseMalformed,
|
|
50329
|
+
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
50330
|
+
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
50331
|
+
malformedHost: baseMalformedHost,
|
|
50332
|
+
malformedScheme: baseMalformedScheme
|
|
50333
|
+
} = parseWithStatus(baseURI, schemelessOptions)
|
|
50334
|
+
const {
|
|
50335
|
+
parsed: relativeParsed,
|
|
50336
|
+
malformedAuthorityOrPort: relativeMalformed,
|
|
50337
|
+
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
50338
|
+
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
50339
|
+
malformedHost: relativeMalformedHost,
|
|
50340
|
+
malformedScheme: relativeMalformedScheme
|
|
50341
|
+
} = parseWithStatus(relativeURI, schemelessOptions)
|
|
50342
|
+
if (
|
|
50343
|
+
baseMalformed ||
|
|
50344
|
+
relativeMalformed ||
|
|
50345
|
+
baseMalformedPercentEncoding ||
|
|
50346
|
+
relativeMalformedPercentEncoding ||
|
|
50347
|
+
baseMalformedSchemeSpecific ||
|
|
50348
|
+
relativeMalformedSchemeSpecific ||
|
|
50349
|
+
baseMalformedHost ||
|
|
50350
|
+
relativeMalformedHost ||
|
|
50351
|
+
baseMalformedScheme ||
|
|
50352
|
+
relativeMalformedScheme
|
|
50353
|
+
) {
|
|
50354
|
+
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
50355
|
+
}
|
|
50356
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
|
|
50357
|
+
const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
|
|
50358
|
+
const resolvedHost = resolved.host
|
|
50359
|
+
const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
|
|
50360
|
+
(isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
|
|
50361
|
+
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
|
|
50362
|
+
// Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
|
|
50363
|
+
// parser can reject them even though fast-uri preserves them safely as RFC
|
|
50364
|
+
// 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
|
|
50365
|
+
const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
|
|
50366
|
+
!/\P{ASCII}/u.test(resolvedHost)
|
|
50367
|
+
if (resolved.error && !encodedASCIIHost) {
|
|
50368
|
+
throw new Error(resolved.error)
|
|
50369
|
+
}
|
|
50140
50370
|
schemelessOptions.skipEscape = true
|
|
50141
50371
|
return serialize(resolved, schemelessOptions)
|
|
50142
50372
|
}
|
|
@@ -50216,21 +50446,10 @@ function resolveComponent (base, relative, options, skipNormalization) {
|
|
|
50216
50446
|
* @returns {boolean}
|
|
50217
50447
|
*/
|
|
50218
50448
|
function equal (uriA, uriB, options) {
|
|
50219
|
-
|
|
50220
|
-
|
|
50221
|
-
uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
|
|
50222
|
-
} else if (typeof uriA === 'object') {
|
|
50223
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
|
|
50224
|
-
}
|
|
50225
|
-
|
|
50226
|
-
if (typeof uriB === 'string') {
|
|
50227
|
-
uriB = unescape(uriB)
|
|
50228
|
-
uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
|
|
50229
|
-
} else if (typeof uriB === 'object') {
|
|
50230
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
|
|
50231
|
-
}
|
|
50449
|
+
const normalizedA = normalizeComparableURI(uriA, options)
|
|
50450
|
+
const normalizedB = normalizeComparableURI(uriB, options)
|
|
50232
50451
|
|
|
50233
|
-
return
|
|
50452
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
|
|
50234
50453
|
}
|
|
50235
50454
|
|
|
50236
50455
|
/**
|
|
@@ -50258,25 +50477,30 @@ function serialize (cmpts, opts) {
|
|
|
50258
50477
|
const options = Object.assign({}, opts)
|
|
50259
50478
|
const uriTokens = []
|
|
50260
50479
|
|
|
50480
|
+
if (component.scheme) {
|
|
50481
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
50482
|
+
}
|
|
50483
|
+
|
|
50261
50484
|
// find scheme handler
|
|
50262
50485
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
|
|
50263
50486
|
|
|
50264
50487
|
// perform scheme specific serialization
|
|
50265
50488
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
|
|
50266
50489
|
|
|
50490
|
+
const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
|
|
50491
|
+
const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
|
|
50492
|
+
|
|
50267
50493
|
if (component.path !== undefined) {
|
|
50268
50494
|
if (!options.skipEscape) {
|
|
50269
|
-
component.path =
|
|
50270
|
-
|
|
50271
|
-
if (component.scheme !== undefined) {
|
|
50272
|
-
component.path = component.path.split('%3A').join(':')
|
|
50273
|
-
}
|
|
50495
|
+
component.path = serializePathEncoding(component.path, pathNoScheme)
|
|
50274
50496
|
} else {
|
|
50275
|
-
component.path =
|
|
50497
|
+
component.path = normalizePercentEncoding(component.path)
|
|
50276
50498
|
}
|
|
50277
50499
|
}
|
|
50278
50500
|
|
|
50279
50501
|
if (options.reference !== 'suffix' && component.scheme) {
|
|
50502
|
+
// Scheme handlers may replace the scheme during serialization.
|
|
50503
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
50280
50504
|
uriTokens.push(component.scheme, ':')
|
|
50281
50505
|
}
|
|
50282
50506
|
|
|
@@ -50299,6 +50523,13 @@ function serialize (cmpts, opts) {
|
|
|
50299
50523
|
s = removeDotSegments(s)
|
|
50300
50524
|
}
|
|
50301
50525
|
|
|
50526
|
+
// Dot-segment removal can expose a colon that was not originally in the
|
|
50527
|
+
// first segment (for example, "./a:b"). Reapply path-noscheme encoding so
|
|
50528
|
+
// the serialized relative reference cannot be reparsed as a URI scheme.
|
|
50529
|
+
if (pathNoScheme) {
|
|
50530
|
+
s = serializePathEncoding(s, true)
|
|
50531
|
+
}
|
|
50532
|
+
|
|
50302
50533
|
if (
|
|
50303
50534
|
authority === undefined &&
|
|
50304
50535
|
s[0] === '/' &&
|
|
@@ -50312,23 +50543,117 @@ function serialize (cmpts, opts) {
|
|
|
50312
50543
|
}
|
|
50313
50544
|
|
|
50314
50545
|
if (component.query !== undefined) {
|
|
50315
|
-
uriTokens.push('?', component.query)
|
|
50546
|
+
uriTokens.push('?', encodeQuery(component.query))
|
|
50316
50547
|
}
|
|
50317
50548
|
|
|
50318
50549
|
if (component.fragment !== undefined) {
|
|
50319
|
-
uriTokens.push('#', component.fragment)
|
|
50550
|
+
uriTokens.push('#', encodeFragment(component.fragment))
|
|
50320
50551
|
}
|
|
50321
50552
|
return uriTokens.join('')
|
|
50322
50553
|
}
|
|
50323
50554
|
|
|
50324
50555
|
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
|
|
50325
50556
|
|
|
50557
|
+
// Captures the authority component (between "//" and the next "/", "?" or "#"),
|
|
50558
|
+
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
50559
|
+
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
50560
|
+
|
|
50561
|
+
// Captures the leading authority-introducer region after an optional scheme: a
|
|
50562
|
+
// run of forward slashes, backslashes, and the characters the WHATWG URL parser
|
|
50563
|
+
// removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
|
|
50564
|
+
// is exactly "//". Node treats "\" as "/" on special schemes and strips those
|
|
50565
|
+
// characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
|
|
50566
|
+
// "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
|
|
50567
|
+
// the path group (host confusion / SSRF / redirect bypass).
|
|
50568
|
+
const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
|
|
50569
|
+
|
|
50570
|
+
/**
|
|
50571
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
50572
|
+
* @param {RegExpMatchArray} matches
|
|
50573
|
+
* @returns {string|undefined}
|
|
50574
|
+
*/
|
|
50575
|
+
function getParseError (parsed, matches) {
|
|
50576
|
+
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
|
|
50577
|
+
return 'URI path must start with "/" when authority is present.'
|
|
50578
|
+
}
|
|
50579
|
+
|
|
50580
|
+
if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
|
|
50581
|
+
return 'URI port is malformed.'
|
|
50582
|
+
}
|
|
50583
|
+
|
|
50584
|
+
return undefined
|
|
50585
|
+
}
|
|
50586
|
+
|
|
50587
|
+
/**
|
|
50588
|
+
* Checks percent syntax without decoding the represented octets. RFC 3986
|
|
50589
|
+
* percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
|
|
50590
|
+
* though they are not independently valid UTF-8.
|
|
50591
|
+
*
|
|
50592
|
+
* @param {string|undefined} component
|
|
50593
|
+
* @returns {boolean}
|
|
50594
|
+
*/
|
|
50595
|
+
function hasMalformedPercentEncoding (component) {
|
|
50596
|
+
if (component === undefined) return false
|
|
50597
|
+
|
|
50598
|
+
let percent = component.indexOf('%')
|
|
50599
|
+
while (percent !== -1) {
|
|
50600
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
50601
|
+
return true
|
|
50602
|
+
}
|
|
50603
|
+
percent = component.indexOf('%', percent + 3)
|
|
50604
|
+
}
|
|
50605
|
+
|
|
50606
|
+
return false
|
|
50607
|
+
}
|
|
50608
|
+
|
|
50609
|
+
/**
|
|
50610
|
+
* @param {RegExpMatchArray} matches
|
|
50611
|
+
* @returns {boolean}
|
|
50612
|
+
*/
|
|
50613
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
50614
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
50615
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
50616
|
+
const host = matches[4]
|
|
50617
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
50618
|
+
(host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
|
|
50619
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
50620
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
50621
|
+
hasMalformedPercentEncoding(matches[8])
|
|
50622
|
+
}
|
|
50623
|
+
|
|
50624
|
+
/**
|
|
50625
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
50626
|
+
* @param {import('./types/index').Options} options
|
|
50627
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
50628
|
+
* @param {boolean} isIP
|
|
50629
|
+
* @returns {boolean} whether host conversion failed
|
|
50630
|
+
*/
|
|
50631
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
50632
|
+
if (
|
|
50633
|
+
!options.unicodeSupport &&
|
|
50634
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
50635
|
+
parsed.host &&
|
|
50636
|
+
parsed.host[0] !== '[' &&
|
|
50637
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
50638
|
+
isIP === false &&
|
|
50639
|
+
nonSimpleDomain(parsed.host)
|
|
50640
|
+
) {
|
|
50641
|
+
try {
|
|
50642
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
50643
|
+
} catch (e) {
|
|
50644
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
50645
|
+
return true
|
|
50646
|
+
}
|
|
50647
|
+
}
|
|
50648
|
+
return false
|
|
50649
|
+
}
|
|
50650
|
+
|
|
50326
50651
|
/**
|
|
50327
50652
|
* @param {string} uri
|
|
50328
50653
|
* @param {import('./types/index').Options} [opts]
|
|
50329
|
-
* @returns
|
|
50654
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
50330
50655
|
*/
|
|
50331
|
-
function
|
|
50656
|
+
function parseWithStatus (uri, opts) {
|
|
50332
50657
|
const options = Object.assign({}, opts)
|
|
50333
50658
|
/** @type {import('./types/index').URIComponent} */
|
|
50334
50659
|
const parsed = {
|
|
@@ -50341,6 +50666,13 @@ function parse (uri, opts) {
|
|
|
50341
50666
|
fragment: undefined
|
|
50342
50667
|
}
|
|
50343
50668
|
|
|
50669
|
+
let malformedAuthorityOrPort = false
|
|
50670
|
+
let malformedPercentEncoding = false
|
|
50671
|
+
let malformedSchemeSpecific = false
|
|
50672
|
+
let malformedHost = false
|
|
50673
|
+
let malformedIPLiteral = false
|
|
50674
|
+
let malformedScheme = false
|
|
50675
|
+
|
|
50344
50676
|
let isIP = false
|
|
50345
50677
|
if (options.reference === 'suffix') {
|
|
50346
50678
|
if (options.scheme) {
|
|
@@ -50350,6 +50682,41 @@ function parse (uri, opts) {
|
|
|
50350
50682
|
}
|
|
50351
50683
|
}
|
|
50352
50684
|
|
|
50685
|
+
// A literal backslash (U+005C) is not a valid RFC 3986 URI character and is
|
|
50686
|
+
// not an authority delimiter. Reject it in the authority rather than
|
|
50687
|
+
// rewriting it: normalizing "\" -> "/" (WHATWG error recovery) could silently
|
|
50688
|
+
// change the resource identified by an otherwise-invalid input, and lets "\"
|
|
50689
|
+
// act as a host delimiter here while Node's native URL parses a different
|
|
50690
|
+
// host (SSRF / redirect / origin-allowlist bypass). Percent-encoded %5C is
|
|
50691
|
+
// untouched and remains valid encoded data.
|
|
50692
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX)
|
|
50693
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf('\\') !== -1) {
|
|
50694
|
+
parsed.error = 'URI authority must not contain a literal backslash.'
|
|
50695
|
+
malformedAuthorityOrPort = true
|
|
50696
|
+
}
|
|
50697
|
+
|
|
50698
|
+
// Reject a malformed or whitespace-smuggled authority introducer. fast-uri
|
|
50699
|
+
// only recognizes a literal "//"; anything else in the leading separator run
|
|
50700
|
+
// (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
|
|
50701
|
+
// Node strips) means the authority fast-uri parses differs from the one Node's
|
|
50702
|
+
// URL resolves. Reject rather than rewrite, mirroring the literal-backslash
|
|
50703
|
+
// guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
|
|
50704
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
|
|
50705
|
+
if (introducerMatch !== null) {
|
|
50706
|
+
const region = introducerMatch[1]
|
|
50707
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, '')
|
|
50708
|
+
// Two or more leading separators introduce an authority.
|
|
50709
|
+
if (normalizedRegion.length >= 2) {
|
|
50710
|
+
if (normalizedRegion.slice(0, 2) !== '//') {
|
|
50711
|
+
parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
|
|
50712
|
+
malformedAuthorityOrPort = true
|
|
50713
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
50714
|
+
parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
|
|
50715
|
+
malformedAuthorityOrPort = true
|
|
50716
|
+
}
|
|
50717
|
+
}
|
|
50718
|
+
}
|
|
50719
|
+
|
|
50353
50720
|
const matches = uri.match(URI_PARSE)
|
|
50354
50721
|
|
|
50355
50722
|
if (matches) {
|
|
@@ -50362,16 +50729,45 @@ function parse (uri, opts) {
|
|
|
50362
50729
|
parsed.query = matches[7]
|
|
50363
50730
|
parsed.fragment = matches[8]
|
|
50364
50731
|
|
|
50732
|
+
if (parsed.scheme !== undefined) {
|
|
50733
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
50734
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
50735
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
50736
|
+
} else {
|
|
50737
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
50738
|
+
malformedScheme = true
|
|
50739
|
+
}
|
|
50740
|
+
}
|
|
50741
|
+
|
|
50742
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
50743
|
+
if (malformedPercentEncoding) {
|
|
50744
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
50745
|
+
}
|
|
50746
|
+
|
|
50365
50747
|
// fix port number
|
|
50366
50748
|
if (isNaN(parsed.port)) {
|
|
50367
50749
|
parsed.port = matches[5]
|
|
50368
50750
|
}
|
|
50751
|
+
|
|
50752
|
+
const parseError = getParseError(parsed, matches)
|
|
50753
|
+
if (parseError !== undefined) {
|
|
50754
|
+
parsed.error = parsed.error || parseError
|
|
50755
|
+
malformedAuthorityOrPort = true
|
|
50756
|
+
}
|
|
50757
|
+
|
|
50369
50758
|
if (parsed.host) {
|
|
50370
50759
|
const ipv4result = isIPv4(parsed.host)
|
|
50371
50760
|
if (ipv4result === false) {
|
|
50761
|
+
const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
|
|
50372
50762
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
50373
|
-
|
|
50374
|
-
|
|
50763
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
50764
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
|
|
50765
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
50766
|
+
|
|
50767
|
+
if (malformedIPLiteral) {
|
|
50768
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
50769
|
+
malformedAuthorityOrPort = true
|
|
50770
|
+
}
|
|
50375
50771
|
} else {
|
|
50376
50772
|
isIP = true
|
|
50377
50773
|
}
|
|
@@ -50394,45 +50790,93 @@ function parse (uri, opts) {
|
|
|
50394
50790
|
// find scheme handler
|
|
50395
50791
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
50396
50792
|
|
|
50397
|
-
//
|
|
50398
|
-
|
|
50399
|
-
// if host component is a domain name
|
|
50400
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
50401
|
-
// convert Unicode IDN -> ASCII IDN
|
|
50402
|
-
try {
|
|
50403
|
-
parsed.host = URL.domainToASCII(parsed.host.toLowerCase())
|
|
50404
|
-
} catch (e) {
|
|
50405
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
50406
|
-
}
|
|
50407
|
-
}
|
|
50408
|
-
// convert IRI -> URI
|
|
50409
|
-
}
|
|
50793
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
50794
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
50410
50795
|
|
|
50411
50796
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
50412
50797
|
if (uri.indexOf('%') !== -1) {
|
|
50413
|
-
if (parsed.
|
|
50414
|
-
parsed.
|
|
50415
|
-
|
|
50416
|
-
if (parsed.host !== undefined) {
|
|
50417
|
-
parsed.host = unescape(parsed.host)
|
|
50798
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
50799
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
50800
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
50418
50801
|
}
|
|
50419
50802
|
}
|
|
50420
50803
|
if (parsed.path) {
|
|
50421
|
-
parsed.path =
|
|
50804
|
+
parsed.path = normalizePathEncoding(parsed.path)
|
|
50805
|
+
}
|
|
50806
|
+
if (parsed.query) {
|
|
50807
|
+
parsed.query = normalizeQueryFragmentEncoding(parsed.query)
|
|
50422
50808
|
}
|
|
50423
50809
|
if (parsed.fragment) {
|
|
50424
|
-
parsed.fragment =
|
|
50810
|
+
parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
|
|
50425
50811
|
}
|
|
50426
50812
|
}
|
|
50427
50813
|
|
|
50428
50814
|
// perform scheme specific parsing
|
|
50429
50815
|
if (schemeHandler && schemeHandler.parse) {
|
|
50430
50816
|
schemeHandler.parse(parsed, options)
|
|
50817
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
50818
|
+
malformedSchemeSpecific = true
|
|
50819
|
+
}
|
|
50431
50820
|
}
|
|
50432
50821
|
} else {
|
|
50433
50822
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
50434
50823
|
}
|
|
50435
|
-
return parsed
|
|
50824
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
50825
|
+
}
|
|
50826
|
+
|
|
50827
|
+
/**
|
|
50828
|
+
* @param {string} uri
|
|
50829
|
+
* @param {import('./types/index').Options} [opts]
|
|
50830
|
+
* @returns
|
|
50831
|
+
*/
|
|
50832
|
+
function parse (uri, opts) {
|
|
50833
|
+
return parseWithStatus(uri, opts).parsed
|
|
50834
|
+
}
|
|
50835
|
+
|
|
50836
|
+
/**
|
|
50837
|
+
* @param {string} uri
|
|
50838
|
+
* @param {import('./types/index').Options} [opts]
|
|
50839
|
+
* @returns {string}
|
|
50840
|
+
*/
|
|
50841
|
+
function normalizeString (uri, opts) {
|
|
50842
|
+
return normalizeStringWithStatus(uri, opts).normalized
|
|
50843
|
+
}
|
|
50844
|
+
|
|
50845
|
+
/**
|
|
50846
|
+
* @param {string} uri
|
|
50847
|
+
* @param {import('./types/index').Options} [opts]
|
|
50848
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
50849
|
+
*/
|
|
50850
|
+
function normalizeStringWithStatus (uri, opts) {
|
|
50851
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
50852
|
+
return {
|
|
50853
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
50854
|
+
malformedAuthorityOrPort,
|
|
50855
|
+
malformedPercentEncoding,
|
|
50856
|
+
malformedSchemeSpecific,
|
|
50857
|
+
malformedHost,
|
|
50858
|
+
malformedScheme
|
|
50859
|
+
}
|
|
50860
|
+
}
|
|
50861
|
+
|
|
50862
|
+
/**
|
|
50863
|
+
* @param {import ('./types/index').URIComponent|string} uri
|
|
50864
|
+
* @param {import('./types/index').Options} [opts]
|
|
50865
|
+
* @returns {string|undefined}
|
|
50866
|
+
*/
|
|
50867
|
+
function normalizeComparableURI (uri, opts) {
|
|
50868
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
50869
|
+
return undefined
|
|
50870
|
+
}
|
|
50871
|
+
|
|
50872
|
+
let value
|
|
50873
|
+
try {
|
|
50874
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
50875
|
+
} catch {
|
|
50876
|
+
return undefined
|
|
50877
|
+
}
|
|
50878
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
50879
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
50436
50880
|
}
|
|
50437
50881
|
|
|
50438
50882
|
const fastUri = {
|
|
@@ -50459,7 +50903,7 @@ module.exports.fastUri = fastUri
|
|
|
50459
50903
|
|
|
50460
50904
|
|
|
50461
50905
|
const { isUUID } = __nccwpck_require__(5077)
|
|
50462
|
-
const URN_REG =
|
|
50906
|
+
const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu
|
|
50463
50907
|
|
|
50464
50908
|
const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
|
|
50465
50909
|
'wss', 'urn', 'urn:uuid'])
|
|
@@ -50571,9 +51015,14 @@ function wsSerialize (wsComponent) {
|
|
|
50571
51015
|
|
|
50572
51016
|
// reconstruct path from resource name
|
|
50573
51017
|
if (wsComponent.resourceName) {
|
|
50574
|
-
const
|
|
51018
|
+
const queryIndex = wsComponent.resourceName.indexOf('?')
|
|
51019
|
+
const path = queryIndex === -1
|
|
51020
|
+
? wsComponent.resourceName
|
|
51021
|
+
: wsComponent.resourceName.slice(0, queryIndex)
|
|
50575
51022
|
wsComponent.path = (path && path !== '/' ? path : undefined)
|
|
50576
|
-
wsComponent.query =
|
|
51023
|
+
wsComponent.query = queryIndex === -1
|
|
51024
|
+
? undefined
|
|
51025
|
+
: wsComponent.resourceName.slice(queryIndex + 1)
|
|
50577
51026
|
wsComponent.resourceName = undefined
|
|
50578
51027
|
}
|
|
50579
51028
|
|
|
@@ -50590,7 +51039,7 @@ function urnParse (urnComponent, options) {
|
|
|
50590
51039
|
return urnComponent
|
|
50591
51040
|
}
|
|
50592
51041
|
const matches = urnComponent.path.match(URN_REG)
|
|
50593
|
-
if (matches) {
|
|
51042
|
+
if (matches && matches[0] === urnComponent.path) {
|
|
50594
51043
|
const scheme = options.scheme || urnComponent.scheme || 'urn'
|
|
50595
51044
|
urnComponent.nid = matches[1].toLowerCase()
|
|
50596
51045
|
urnComponent.nss = matches[2]
|
|
@@ -50739,6 +51188,44 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
|
|
|
50739
51188
|
/** @type {(value: string) => boolean} */
|
|
50740
51189
|
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)
|
|
50741
51190
|
|
|
51191
|
+
/** @type {(value: string) => boolean} */
|
|
51192
|
+
const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
51193
|
+
|
|
51194
|
+
/** @type {(value: string) => boolean} */
|
|
51195
|
+
const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
|
|
51196
|
+
|
|
51197
|
+
/** @type {(value: string) => boolean} */
|
|
51198
|
+
const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
|
|
51199
|
+
|
|
51200
|
+
/** @type {(value: string) => boolean} */
|
|
51201
|
+
const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
|
|
51202
|
+
|
|
51203
|
+
/** @type {(value: string) => boolean} */
|
|
51204
|
+
const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
|
|
51205
|
+
|
|
51206
|
+
const BYTE_HEX = new Array(256)
|
|
51207
|
+
{
|
|
51208
|
+
const HEX_DIGITS = '0123456789ABCDEF'
|
|
51209
|
+
for (let i = 0; i < 256; i++) {
|
|
51210
|
+
BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
|
|
51211
|
+
}
|
|
51212
|
+
}
|
|
51213
|
+
function percentEncodeNonAscii (cp) {
|
|
51214
|
+
if (cp < 0x800) {
|
|
51215
|
+
return BYTE_HEX[0xC0 | (cp >> 6)] +
|
|
51216
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
51217
|
+
}
|
|
51218
|
+
if (cp < 0x10000) {
|
|
51219
|
+
return BYTE_HEX[0xE0 | (cp >> 12)] +
|
|
51220
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
51221
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
51222
|
+
}
|
|
51223
|
+
return BYTE_HEX[0xF0 | (cp >> 18)] +
|
|
51224
|
+
BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
|
|
51225
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
51226
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
51227
|
+
}
|
|
51228
|
+
|
|
50742
51229
|
/**
|
|
50743
51230
|
* @param {Array<string>} input
|
|
50744
51231
|
* @returns {string}
|
|
@@ -50770,12 +51257,14 @@ function stringArrayToHexStripped (input) {
|
|
|
50770
51257
|
return acc
|
|
50771
51258
|
}
|
|
50772
51259
|
|
|
50773
|
-
/**
|
|
50774
|
-
|
|
50775
|
-
|
|
50776
|
-
|
|
50777
|
-
|
|
50778
|
-
|
|
51260
|
+
/** @type {(value: string) => boolean} */
|
|
51261
|
+
const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
|
|
51262
|
+
|
|
51263
|
+
/** @type {(value: string) => boolean} */
|
|
51264
|
+
const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
|
|
51265
|
+
|
|
51266
|
+
/** @type {(value: string) => boolean} */
|
|
51267
|
+
const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
|
|
50779
51268
|
|
|
50780
51269
|
/**
|
|
50781
51270
|
* @param {string} value
|
|
@@ -50784,88 +51273,104 @@ function stringArrayToHexStripped (input) {
|
|
|
50784
51273
|
const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
|
|
50785
51274
|
|
|
50786
51275
|
/**
|
|
50787
|
-
* @param {
|
|
51276
|
+
* @param {string} zone
|
|
50788
51277
|
* @returns {boolean}
|
|
50789
51278
|
*/
|
|
50790
|
-
function
|
|
50791
|
-
|
|
51279
|
+
function isZoneIdentifier (zone) {
|
|
51280
|
+
if (zone.length === 0) return false
|
|
51281
|
+
|
|
51282
|
+
for (let i = 0; i < zone.length; i++) {
|
|
51283
|
+
if (isZoneCharacter(zone[i])) continue
|
|
51284
|
+
if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
|
|
51285
|
+
i += 2
|
|
51286
|
+
continue
|
|
51287
|
+
}
|
|
51288
|
+
return false
|
|
51289
|
+
}
|
|
51290
|
+
|
|
50792
51291
|
return true
|
|
50793
51292
|
}
|
|
50794
51293
|
|
|
50795
51294
|
/**
|
|
50796
|
-
*
|
|
50797
|
-
*
|
|
50798
|
-
*
|
|
50799
|
-
* @
|
|
51295
|
+
* Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
|
|
51296
|
+
* single zero hextet is left uncompressed. On ties the leftmost run wins.
|
|
51297
|
+
*
|
|
51298
|
+
* @param {string[]} hextets
|
|
51299
|
+
* @returns {string}
|
|
50800
51300
|
*/
|
|
50801
|
-
function
|
|
50802
|
-
|
|
50803
|
-
|
|
50804
|
-
|
|
50805
|
-
|
|
51301
|
+
function compressIPv6ZeroRun (hextets) {
|
|
51302
|
+
let bestStart = -1
|
|
51303
|
+
let bestLength = 0
|
|
51304
|
+
let runStart = -1
|
|
51305
|
+
let runLength = 0
|
|
51306
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
51307
|
+
if (hextets[i] === '0') {
|
|
51308
|
+
if (runStart === -1) runStart = i
|
|
51309
|
+
runLength++
|
|
51310
|
+
if (runLength > bestLength) {
|
|
51311
|
+
bestLength = runLength
|
|
51312
|
+
bestStart = runStart
|
|
51313
|
+
}
|
|
50806
51314
|
} else {
|
|
50807
|
-
|
|
50808
|
-
|
|
51315
|
+
runStart = -1
|
|
51316
|
+
runLength = 0
|
|
50809
51317
|
}
|
|
50810
|
-
buffer.length = 0
|
|
50811
51318
|
}
|
|
50812
|
-
|
|
51319
|
+
|
|
51320
|
+
if (bestLength < 2) return hextets.join(':')
|
|
51321
|
+
|
|
51322
|
+
const head = hextets.slice(0, bestStart).join(':')
|
|
51323
|
+
const tail = hextets.slice(bestStart + bestLength).join(':')
|
|
51324
|
+
return head + '::' + tail
|
|
50813
51325
|
}
|
|
50814
51326
|
|
|
50815
51327
|
/**
|
|
51328
|
+
* Validates an IPv6 address against the alternatives in RFC 3986 section
|
|
51329
|
+
* 3.2.2 and returns the same address with leading hextet zeroes removed.
|
|
51330
|
+
* An embedded IPv4 address counts as two hextets and is only valid at the end.
|
|
51331
|
+
*
|
|
50816
51332
|
* @param {string} input
|
|
50817
|
-
* @returns {
|
|
51333
|
+
* @returns {string|undefined}
|
|
50818
51334
|
*/
|
|
50819
|
-
function
|
|
50820
|
-
|
|
50821
|
-
|
|
50822
|
-
|
|
50823
|
-
const
|
|
50824
|
-
|
|
50825
|
-
|
|
50826
|
-
|
|
50827
|
-
|
|
50828
|
-
|
|
50829
|
-
|
|
50830
|
-
|
|
50831
|
-
|
|
50832
|
-
|
|
50833
|
-
|
|
50834
|
-
if (
|
|
50835
|
-
|
|
50836
|
-
|
|
50837
|
-
|
|
50838
|
-
|
|
50839
|
-
if (++tokenCount > 7) {
|
|
50840
|
-
// not valid
|
|
50841
|
-
output.error = true
|
|
50842
|
-
break
|
|
50843
|
-
}
|
|
50844
|
-
if (i > 0 && input[i - 1] === ':') {
|
|
50845
|
-
endipv6Encountered = true
|
|
50846
|
-
}
|
|
50847
|
-
address.push(':')
|
|
50848
|
-
continue
|
|
50849
|
-
} else if (cursor === '%') {
|
|
50850
|
-
if (!consume(buffer, address, output)) { break }
|
|
50851
|
-
// switch to zone detection
|
|
50852
|
-
consume = consumeIsZone
|
|
50853
|
-
} else {
|
|
50854
|
-
buffer.push(cursor)
|
|
51335
|
+
function normalizeIPv6Address (input) {
|
|
51336
|
+
const compression = input.indexOf('::')
|
|
51337
|
+
if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
|
|
51338
|
+
|
|
51339
|
+
const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
|
|
51340
|
+
const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
|
|
51341
|
+
if (compression !== -1) {
|
|
51342
|
+
if (left.length === 1 && left[0] === '') left.length = 0
|
|
51343
|
+
if (right.length === 1 && right[0] === '') right.length = 0
|
|
51344
|
+
}
|
|
51345
|
+
|
|
51346
|
+
const parts = left.concat(right)
|
|
51347
|
+
let hextetCount = 0
|
|
51348
|
+
for (let i = 0; i < parts.length; i++) {
|
|
51349
|
+
const part = parts[i]
|
|
51350
|
+
if (part === '') return undefined
|
|
51351
|
+
|
|
51352
|
+
if (part.indexOf('.') !== -1) {
|
|
51353
|
+
if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
|
|
51354
|
+
hextetCount += 2
|
|
50855
51355
|
continue
|
|
50856
51356
|
}
|
|
51357
|
+
|
|
51358
|
+
if (!isHextet(part)) return undefined
|
|
51359
|
+
parts[i] = parseInt(part, 16).toString(16)
|
|
51360
|
+
hextetCount++
|
|
50857
51361
|
}
|
|
50858
|
-
|
|
50859
|
-
|
|
50860
|
-
|
|
50861
|
-
|
|
50862
|
-
address.push(buffer.join(''))
|
|
50863
|
-
} else {
|
|
50864
|
-
address.push(stringArrayToHexStripped(buffer))
|
|
50865
|
-
}
|
|
51362
|
+
|
|
51363
|
+
if (compression === -1) {
|
|
51364
|
+
if (hextetCount !== 8) return undefined
|
|
51365
|
+
return compressIPv6ZeroRun(parts)
|
|
50866
51366
|
}
|
|
50867
|
-
|
|
50868
|
-
|
|
51367
|
+
if (hextetCount >= 8) return undefined
|
|
51368
|
+
|
|
51369
|
+
// expand "::" then re-compress the longest run for a canonical result
|
|
51370
|
+
const expanded = parts.slice(0, left.length)
|
|
51371
|
+
for (let i = hextetCount; i < 8; i++) expanded.push('0')
|
|
51372
|
+
for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
|
|
51373
|
+
return compressIPv6ZeroRun(expanded)
|
|
50869
51374
|
}
|
|
50870
51375
|
|
|
50871
51376
|
/**
|
|
@@ -50873,26 +51378,49 @@ function getIPV6 (input) {
|
|
|
50873
51378
|
* @property {string} host - The normalized host.
|
|
50874
51379
|
* @property {string} [escapedHost] - The escaped host.
|
|
50875
51380
|
* @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
|
|
51381
|
+
* @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
|
|
51382
|
+
* @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
|
|
50876
51383
|
*/
|
|
50877
51384
|
|
|
50878
51385
|
/**
|
|
51386
|
+
* Validates and normalizes a bracketed IP literal. Raw zone separators remain
|
|
51387
|
+
* accepted for backwards compatibility, while encoded separators and zone
|
|
51388
|
+
* contents follow RFC 6874.
|
|
51389
|
+
*
|
|
50879
51390
|
* @param {string} host
|
|
50880
51391
|
* @returns {NormalizeIPv6Result}
|
|
50881
51392
|
*/
|
|
50882
51393
|
function normalizeIPv6 (host) {
|
|
50883
|
-
|
|
50884
|
-
const
|
|
51394
|
+
const bracketed = host[0] === '[' && host[host.length - 1] === ']'
|
|
51395
|
+
const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
|
|
51396
|
+
if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
|
|
50885
51397
|
|
|
50886
|
-
|
|
50887
|
-
|
|
50888
|
-
|
|
50889
|
-
|
|
50890
|
-
|
|
50891
|
-
|
|
50892
|
-
|
|
50893
|
-
return { host
|
|
50894
|
-
}
|
|
50895
|
-
|
|
51398
|
+
let input = bracketed ? host.slice(1, -1) : host
|
|
51399
|
+
if (bracketed && isIPvFuture(input)) {
|
|
51400
|
+
input = input.toLowerCase()
|
|
51401
|
+
return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
|
|
51402
|
+
}
|
|
51403
|
+
|
|
51404
|
+
if (findToken(input, ':') < 2) {
|
|
51405
|
+
return { host, isIPV6: false, error: bracketed }
|
|
51406
|
+
}
|
|
51407
|
+
|
|
51408
|
+
let zoneIdentifier = ''
|
|
51409
|
+
const zoneSeparator = input.indexOf('%')
|
|
51410
|
+
if (zoneSeparator !== -1) {
|
|
51411
|
+
const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
|
|
51412
|
+
zoneIdentifier = input.slice(zoneSeparator + separatorLength)
|
|
51413
|
+
if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
|
|
51414
|
+
input = input.slice(0, zoneSeparator)
|
|
51415
|
+
}
|
|
51416
|
+
|
|
51417
|
+
const address = normalizeIPv6Address(input)
|
|
51418
|
+
if (address === undefined) return { host, isIPV6: false, error: true }
|
|
51419
|
+
|
|
51420
|
+
return {
|
|
51421
|
+
host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
|
|
51422
|
+
escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
|
|
51423
|
+
isIPV6: true
|
|
50896
51424
|
}
|
|
50897
51425
|
}
|
|
50898
51426
|
|
|
@@ -50997,31 +51525,342 @@ function removeDotSegments (path) {
|
|
|
50997
51525
|
}
|
|
50998
51526
|
|
|
50999
51527
|
/**
|
|
51000
|
-
*
|
|
51001
|
-
*
|
|
51002
|
-
*
|
|
51528
|
+
* Re-escape RFC 3986 gen-delims that must not appear literally in the host.
|
|
51529
|
+
* After the URI regex parses, these characters cannot be literal in the host
|
|
51530
|
+
* field, so any that appear after decoding came from percent-encoding and
|
|
51531
|
+
* must be restored to prevent authority structure changes.
|
|
51532
|
+
*
|
|
51533
|
+
* @param {string} host
|
|
51534
|
+
* @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
|
|
51535
|
+
* @returns {string}
|
|
51536
|
+
*/
|
|
51537
|
+
const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
|
|
51538
|
+
const HOST_DELIM_RE = /[@/?#:]/g
|
|
51539
|
+
const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
|
|
51540
|
+
|
|
51541
|
+
function reescapeHostDelimiters (host, isIP) {
|
|
51542
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
|
|
51543
|
+
re.lastIndex = 0
|
|
51544
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch])
|
|
51545
|
+
}
|
|
51546
|
+
|
|
51547
|
+
/**
|
|
51548
|
+
* Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
|
|
51549
|
+
* Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
|
|
51550
|
+
*
|
|
51551
|
+
* @param {string} input
|
|
51552
|
+
* @param {boolean} [decodeUnreserved=false]
|
|
51553
|
+
* @returns {string}
|
|
51003
51554
|
*/
|
|
51004
|
-
function
|
|
51005
|
-
|
|
51006
|
-
|
|
51007
|
-
component.scheme = func(component.scheme)
|
|
51555
|
+
function normalizePercentEncoding (input, decodeUnreserved = false) {
|
|
51556
|
+
if (input.indexOf('%') === -1) {
|
|
51557
|
+
return input
|
|
51008
51558
|
}
|
|
51009
|
-
|
|
51010
|
-
|
|
51559
|
+
|
|
51560
|
+
let output = ''
|
|
51561
|
+
|
|
51562
|
+
for (let i = 0; i < input.length; i++) {
|
|
51563
|
+
if (input[i] === '%' && i + 2 < input.length) {
|
|
51564
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51565
|
+
if (isHexPair(hex)) {
|
|
51566
|
+
const normalizedHex = hex.toUpperCase()
|
|
51567
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
51568
|
+
|
|
51569
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
51570
|
+
output += decoded
|
|
51571
|
+
} else {
|
|
51572
|
+
output += '%' + normalizedHex
|
|
51573
|
+
}
|
|
51574
|
+
|
|
51575
|
+
i += 2
|
|
51576
|
+
continue
|
|
51577
|
+
}
|
|
51578
|
+
}
|
|
51579
|
+
|
|
51580
|
+
output += input[i]
|
|
51011
51581
|
}
|
|
51012
|
-
|
|
51013
|
-
|
|
51582
|
+
|
|
51583
|
+
return output
|
|
51584
|
+
}
|
|
51585
|
+
|
|
51586
|
+
/**
|
|
51587
|
+
* Normalizes path data without turning reserved escapes into live path syntax.
|
|
51588
|
+
* Valid escapes are uppercased, raw unsafe characters are escaped, and only
|
|
51589
|
+
* unreserved bytes that are not `.` are decoded.
|
|
51590
|
+
*
|
|
51591
|
+
* @param {string} input
|
|
51592
|
+
* @returns {string}
|
|
51593
|
+
*/
|
|
51594
|
+
function normalizePathEncoding (input) {
|
|
51595
|
+
let output = ''
|
|
51596
|
+
|
|
51597
|
+
for (let i = 0; i < input.length; i++) {
|
|
51598
|
+
const ch = input[i]
|
|
51599
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
51600
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51601
|
+
if (isHexPair(hex)) {
|
|
51602
|
+
const normalizedHex = hex.toUpperCase()
|
|
51603
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
51604
|
+
|
|
51605
|
+
if (decoded !== '.' && isUnreserved(decoded)) {
|
|
51606
|
+
output += decoded
|
|
51607
|
+
} else {
|
|
51608
|
+
output += '%' + normalizedHex
|
|
51609
|
+
}
|
|
51610
|
+
|
|
51611
|
+
i += 2
|
|
51612
|
+
continue
|
|
51613
|
+
}
|
|
51614
|
+
}
|
|
51615
|
+
|
|
51616
|
+
if (isPathCharacter(ch)) {
|
|
51617
|
+
output += ch
|
|
51618
|
+
} else {
|
|
51619
|
+
const code = input.charCodeAt(i)
|
|
51620
|
+
if (code < 0x80) {
|
|
51621
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
51622
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
51623
|
+
output += percentEncodeNonAscii(code)
|
|
51624
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
51625
|
+
const low = input.charCodeAt(i + 1)
|
|
51626
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
51627
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
51628
|
+
i++
|
|
51629
|
+
} else {
|
|
51630
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51631
|
+
}
|
|
51632
|
+
} else {
|
|
51633
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51634
|
+
}
|
|
51635
|
+
}
|
|
51014
51636
|
}
|
|
51015
|
-
|
|
51016
|
-
|
|
51637
|
+
|
|
51638
|
+
return output
|
|
51639
|
+
}
|
|
51640
|
+
|
|
51641
|
+
/**
|
|
51642
|
+
* Serializes a path without rewriting reserved data. Raw RFC 3986 path
|
|
51643
|
+
* characters remain literal, valid escapes are preserved and uppercased, and
|
|
51644
|
+
* everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
|
|
51645
|
+
* first segment must be escaped so the result cannot be parsed as a scheme.
|
|
51646
|
+
*
|
|
51647
|
+
* @param {string} input
|
|
51648
|
+
* @param {boolean} [pathNoScheme=false]
|
|
51649
|
+
* @returns {string}
|
|
51650
|
+
*/
|
|
51651
|
+
function serializePathEncoding (input, pathNoScheme = false) {
|
|
51652
|
+
let output = ''
|
|
51653
|
+
let firstSegment = pathNoScheme && input[0] !== '/'
|
|
51654
|
+
|
|
51655
|
+
for (let i = 0; i < input.length; i++) {
|
|
51656
|
+
const ch = input[i]
|
|
51657
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
51658
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51659
|
+
if (isHexPair(hex)) {
|
|
51660
|
+
output += '%' + hex.toUpperCase()
|
|
51661
|
+
i += 2
|
|
51662
|
+
continue
|
|
51663
|
+
}
|
|
51664
|
+
}
|
|
51665
|
+
|
|
51666
|
+
if (ch === '/') {
|
|
51667
|
+
firstSegment = false
|
|
51668
|
+
}
|
|
51669
|
+
|
|
51670
|
+
if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
|
|
51671
|
+
output += ch
|
|
51672
|
+
} else {
|
|
51673
|
+
const code = input.charCodeAt(i)
|
|
51674
|
+
if (code < 0x80) {
|
|
51675
|
+
output += BYTE_HEX[code]
|
|
51676
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
51677
|
+
output += percentEncodeNonAscii(code)
|
|
51678
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
51679
|
+
const low = input.charCodeAt(i + 1)
|
|
51680
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
51681
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
51682
|
+
i++
|
|
51683
|
+
} else {
|
|
51684
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51685
|
+
}
|
|
51686
|
+
} else {
|
|
51687
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51688
|
+
}
|
|
51689
|
+
}
|
|
51017
51690
|
}
|
|
51018
|
-
|
|
51019
|
-
|
|
51691
|
+
|
|
51692
|
+
return output
|
|
51693
|
+
}
|
|
51694
|
+
|
|
51695
|
+
/**
|
|
51696
|
+
* Percent-encodes a URI component using its RFC 3986 literal character set.
|
|
51697
|
+
* Existing valid escapes are preserved and normalized to uppercase hex.
|
|
51698
|
+
*
|
|
51699
|
+
* @param {string} input
|
|
51700
|
+
* @param {(value: string) => boolean} isAllowed
|
|
51701
|
+
* @returns {string}
|
|
51702
|
+
*/
|
|
51703
|
+
function encodeComponent (input, isAllowed) {
|
|
51704
|
+
let output = ''
|
|
51705
|
+
|
|
51706
|
+
for (let i = 0; i < input.length; i++) {
|
|
51707
|
+
const ch = input[i]
|
|
51708
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
51709
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51710
|
+
if (isHexPair(hex)) {
|
|
51711
|
+
output += '%' + hex.toUpperCase()
|
|
51712
|
+
i += 2
|
|
51713
|
+
continue
|
|
51714
|
+
}
|
|
51715
|
+
}
|
|
51716
|
+
|
|
51717
|
+
if (isAllowed(ch)) {
|
|
51718
|
+
output += ch
|
|
51719
|
+
} else {
|
|
51720
|
+
const code = input.charCodeAt(i)
|
|
51721
|
+
if (code < 0x80) {
|
|
51722
|
+
output += BYTE_HEX[code]
|
|
51723
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
51724
|
+
output += percentEncodeNonAscii(code)
|
|
51725
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
51726
|
+
const low = input.charCodeAt(i + 1)
|
|
51727
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
51728
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
51729
|
+
i++
|
|
51730
|
+
} else {
|
|
51731
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51732
|
+
}
|
|
51733
|
+
} else {
|
|
51734
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51735
|
+
}
|
|
51736
|
+
}
|
|
51020
51737
|
}
|
|
51021
|
-
|
|
51022
|
-
|
|
51738
|
+
|
|
51739
|
+
return output
|
|
51740
|
+
}
|
|
51741
|
+
|
|
51742
|
+
/**
|
|
51743
|
+
* Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
|
|
51744
|
+
* In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
|
|
51745
|
+
*
|
|
51746
|
+
* @param {string} input
|
|
51747
|
+
* @returns {string}
|
|
51748
|
+
*/
|
|
51749
|
+
function encodeUserinfo (input) {
|
|
51750
|
+
return encodeComponent(input, isUserinfoCharacter)
|
|
51751
|
+
}
|
|
51752
|
+
|
|
51753
|
+
/**
|
|
51754
|
+
* Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
|
|
51755
|
+
* escaped because it would otherwise begin the fragment component.
|
|
51756
|
+
*
|
|
51757
|
+
* @param {string} input
|
|
51758
|
+
* @returns {string}
|
|
51759
|
+
*/
|
|
51760
|
+
function encodeQuery (input) {
|
|
51761
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
51762
|
+
}
|
|
51763
|
+
|
|
51764
|
+
/**
|
|
51765
|
+
* Encodes fragment data using the RFC 3986 §3.5 grammar.
|
|
51766
|
+
*
|
|
51767
|
+
* @param {string} input
|
|
51768
|
+
* @returns {string}
|
|
51769
|
+
*/
|
|
51770
|
+
function encodeFragment (input) {
|
|
51771
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
51772
|
+
}
|
|
51773
|
+
|
|
51774
|
+
function isEscapeSafe (cp) {
|
|
51775
|
+
return (
|
|
51776
|
+
(cp >= 0x30 && cp <= 0x39) ||
|
|
51777
|
+
(cp >= 0x41 && cp <= 0x5A) ||
|
|
51778
|
+
(cp >= 0x61 && cp <= 0x7A) ||
|
|
51779
|
+
cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
|
|
51780
|
+
cp === 0x2F || cp === 0x40 || cp === 0x5F
|
|
51781
|
+
)
|
|
51782
|
+
}
|
|
51783
|
+
|
|
51784
|
+
/**
|
|
51785
|
+
* Normalizes the percent-encoding of a query or fragment component.
|
|
51786
|
+
*
|
|
51787
|
+
* Like `normalizePathEncoding`, but uses the query/fragment character set
|
|
51788
|
+
* (which additionally allows `?`) and decodes `.` since it has no dot-segment
|
|
51789
|
+
* meaning outside of a path.
|
|
51790
|
+
*
|
|
51791
|
+
* @param {string} input
|
|
51792
|
+
* @returns {string}
|
|
51793
|
+
*/
|
|
51794
|
+
function normalizeQueryFragmentEncoding (input) {
|
|
51795
|
+
let output = ''
|
|
51796
|
+
|
|
51797
|
+
for (let i = 0; i < input.length; i++) {
|
|
51798
|
+
const ch = input[i]
|
|
51799
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
51800
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51801
|
+
if (isHexPair(hex)) {
|
|
51802
|
+
const normalizedHex = hex.toUpperCase()
|
|
51803
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
51804
|
+
|
|
51805
|
+
if (isUnreserved(decoded)) {
|
|
51806
|
+
output += decoded
|
|
51807
|
+
} else {
|
|
51808
|
+
output += '%' + normalizedHex
|
|
51809
|
+
}
|
|
51810
|
+
|
|
51811
|
+
i += 2
|
|
51812
|
+
continue
|
|
51813
|
+
}
|
|
51814
|
+
}
|
|
51815
|
+
|
|
51816
|
+
if (isQueryFragmentCharacter(ch)) {
|
|
51817
|
+
output += ch
|
|
51818
|
+
} else {
|
|
51819
|
+
const code = input.charCodeAt(i)
|
|
51820
|
+
if (code < 0x80) {
|
|
51821
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
51822
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
51823
|
+
output += percentEncodeNonAscii(code)
|
|
51824
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
51825
|
+
const low = input.charCodeAt(i + 1)
|
|
51826
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
51827
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
51828
|
+
i++
|
|
51829
|
+
} else {
|
|
51830
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51831
|
+
}
|
|
51832
|
+
} else {
|
|
51833
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
51834
|
+
}
|
|
51835
|
+
}
|
|
51023
51836
|
}
|
|
51024
|
-
|
|
51837
|
+
|
|
51838
|
+
return output
|
|
51839
|
+
}
|
|
51840
|
+
|
|
51841
|
+
/**
|
|
51842
|
+
* Escapes a component while preserving existing valid percent escapes.
|
|
51843
|
+
*
|
|
51844
|
+
* @param {string} input
|
|
51845
|
+
* @returns {string}
|
|
51846
|
+
*/
|
|
51847
|
+
function escapePreservingEscapes (input) {
|
|
51848
|
+
let output = ''
|
|
51849
|
+
|
|
51850
|
+
for (let i = 0; i < input.length; i++) {
|
|
51851
|
+
if (input[i] === '%' && i + 2 < input.length) {
|
|
51852
|
+
const hex = input.slice(i + 1, i + 3)
|
|
51853
|
+
if (isHexPair(hex)) {
|
|
51854
|
+
output += '%' + hex.toUpperCase()
|
|
51855
|
+
i += 2
|
|
51856
|
+
continue
|
|
51857
|
+
}
|
|
51858
|
+
}
|
|
51859
|
+
|
|
51860
|
+
output += escape(input[i])
|
|
51861
|
+
}
|
|
51862
|
+
|
|
51863
|
+
return output
|
|
51025
51864
|
}
|
|
51026
51865
|
|
|
51027
51866
|
/**
|
|
@@ -51032,18 +51871,24 @@ function recomposeAuthority (component) {
|
|
|
51032
51871
|
const uriTokens = []
|
|
51033
51872
|
|
|
51034
51873
|
if (component.userinfo !== undefined) {
|
|
51035
|
-
uriTokens.push(component.userinfo)
|
|
51874
|
+
uriTokens.push(encodeUserinfo(component.userinfo))
|
|
51036
51875
|
uriTokens.push('@')
|
|
51037
51876
|
}
|
|
51038
51877
|
|
|
51039
51878
|
if (component.host !== undefined) {
|
|
51040
|
-
let host =
|
|
51879
|
+
let host = component.host
|
|
51041
51880
|
if (!isIPv4(host)) {
|
|
51042
|
-
|
|
51043
|
-
if (ipV6res.isIPV6
|
|
51881
|
+
let ipV6res = normalizeIPv6(host)
|
|
51882
|
+
if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
|
|
51883
|
+
// Decode only unreserved bytes, once. In particular, keep %25 encoded
|
|
51884
|
+
// so it cannot introduce a second escape during recomposition.
|
|
51885
|
+
host = normalizePercentEncoding(host, true)
|
|
51886
|
+
ipV6res = normalizeIPv6(host)
|
|
51887
|
+
}
|
|
51888
|
+
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
51044
51889
|
host = `[${ipV6res.escapedHost}]`
|
|
51045
51890
|
} else {
|
|
51046
|
-
host =
|
|
51891
|
+
host = reescapeHostDelimiters(host, false)
|
|
51047
51892
|
}
|
|
51048
51893
|
}
|
|
51049
51894
|
uriTokens.push(host)
|
|
@@ -51060,7 +51905,15 @@ function recomposeAuthority (component) {
|
|
|
51060
51905
|
module.exports = {
|
|
51061
51906
|
nonSimpleDomain,
|
|
51062
51907
|
recomposeAuthority,
|
|
51063
|
-
|
|
51908
|
+
reescapeHostDelimiters,
|
|
51909
|
+
normalizePercentEncoding,
|
|
51910
|
+
normalizePathEncoding,
|
|
51911
|
+
serializePathEncoding,
|
|
51912
|
+
normalizeQueryFragmentEncoding,
|
|
51913
|
+
encodeUserinfo,
|
|
51914
|
+
encodeQuery,
|
|
51915
|
+
encodeFragment,
|
|
51916
|
+
escapePreservingEscapes,
|
|
51064
51917
|
removeDotSegments,
|
|
51065
51918
|
isIPv4,
|
|
51066
51919
|
isUUID,
|
|
@@ -72013,7 +72866,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
|
|
|
72013
72866
|
/***/ ((module) => {
|
|
72014
72867
|
|
|
72015
72868
|
"use strict";
|
|
72016
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.
|
|
72869
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.21.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"},"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"}}');
|
|
72017
72870
|
|
|
72018
72871
|
/***/ })
|
|
72019
72872
|
|
|
@@ -72194,7 +73047,7 @@ function startMcpServer() {
|
|
|
72194
73047
|
'A failing verdict names the exact blockers; repair them one targeted question at a time. Never block work on a failing verdict. ' +
|
|
72195
73048
|
'To sync with a shared Pathmode workspace later, run: npx @pathmode/mcp-server setup pm_live_xxx';
|
|
72196
73049
|
const CLOUD_MODE_INSTRUCTIONS = 'Pathmode is connected to a workspace (cloud mode). ' +
|
|
72197
|
-
'First move: call check_intent_readiness before implementation starts. With no arguments it
|
|
73050
|
+
'First move: call check_intent_readiness before implementation starts. With no arguments it resolves this repo\'s intent.md from MCP workspace roots or the server launch directory. It falls back to the workspace current intent only after client-declared roots show that no repo-bound file exists; when the repository cannot be identified it refuses to guess. A failing verdict names the exact blockers to repair. ' +
|
|
72198
73051
|
'If tools fail with API error (401), the configured key is invalid: re-run npx @pathmode/mcp-server setup with a fresh key from your workspace settings at https://pathmode.io, ' +
|
|
72199
73052
|
'or remove PATHMODE_API_KEY and restart to fall back to free keyless local mode (specs live in intent.md, no account needed).';
|
|
72200
73053
|
const server = new mcp_js_1.McpServer({
|
|
@@ -72338,6 +73191,39 @@ function startMcpServer() {
|
|
|
72338
73191
|
return anyReady;
|
|
72339
73192
|
return intents[0];
|
|
72340
73193
|
}
|
|
73194
|
+
/**
|
|
73195
|
+
* Resolve the intent bound to the repository the MCP client says it is working in.
|
|
73196
|
+
*
|
|
73197
|
+
* Global MCP configurations launch the server from an arbitrary directory, so cwd is only a
|
|
73198
|
+
* fallback for clients that do not advertise roots. Declared roots win even when cwd happens to
|
|
73199
|
+
* contain a different intent.md. If roots cannot be resolved and cwd has no intent.md, callers
|
|
73200
|
+
* must not silently substitute the workspace's heuristic "current" intent — that is how a
|
|
73201
|
+
* preflight for one repository graded the demo shoe-return intent instead.
|
|
73202
|
+
*/
|
|
73203
|
+
async function resolveRepoBoundIntent(intentId) {
|
|
73204
|
+
const canListRoots = !!server.server.getClientCapabilities()?.roots;
|
|
73205
|
+
if (canListRoots) {
|
|
73206
|
+
const dirs = await (0, repo_id_1.resolveWorkspaceDirs)(() => server.server.listRoots(undefined, { timeout: 2000 }), {});
|
|
73207
|
+
if (dirs.length > 0) {
|
|
73208
|
+
const candidates = Array.from(new Set(dirs.map(dir => (0, path_1.resolve)(dir, 'intent.md'))))
|
|
73209
|
+
.map(path => ({ path, intent: (0, local_reader_1.readIntentFile)(path) }))
|
|
73210
|
+
.filter((candidate) => !!candidate.intent);
|
|
73211
|
+
const matching = intentId
|
|
73212
|
+
? candidates.filter(candidate => candidate.intent.id === intentId)
|
|
73213
|
+
: candidates;
|
|
73214
|
+
if (matching.length === 1)
|
|
73215
|
+
return { kind: 'found', ...matching[0] };
|
|
73216
|
+
if (matching.length > 1)
|
|
73217
|
+
return { kind: 'ambiguous', paths: matching.map(candidate => candidate.path) };
|
|
73218
|
+
return { kind: 'absent', rootsInspected: true };
|
|
73219
|
+
}
|
|
73220
|
+
}
|
|
73221
|
+
const path = (0, path_1.resolve)(process.cwd(), 'intent.md');
|
|
73222
|
+
const intent = (0, local_reader_1.readIntentFile)(path);
|
|
73223
|
+
if (intent && (!intentId || intent.id === intentId))
|
|
73224
|
+
return { kind: 'found', intent, path };
|
|
73225
|
+
return { kind: 'absent', rootsInspected: false };
|
|
73226
|
+
}
|
|
72341
73227
|
/** The union of evidence IDs an intent cites: directly linked + section anchors + decision-cited.
|
|
72342
73228
|
* Mirrors lib/intentSpecHelpers.collectSpecEvidenceIds (the MCP package can't import app/lib). */
|
|
72343
73229
|
function collectCitedEvidenceIds(intent) {
|
|
@@ -72473,6 +73359,61 @@ function startMcpServer() {
|
|
|
72473
73359
|
return { content: [{ type: 'text', text: `Failed to fetch intent: ${e.message}` }] };
|
|
72474
73360
|
}
|
|
72475
73361
|
});
|
|
73362
|
+
cloudOnly.registerTool('list_intent_change_requests', {
|
|
73363
|
+
title: 'List Intent Change Requests',
|
|
73364
|
+
description: 'List structured PM requests waiting on this repository. Omit intentId to discover open requests across the connected workspace; pass it to narrow to one intent. Read these before editing intent.md. Each open request is bound to one baseRepoBodyRevision; pass its id and base revision to intent_save when you deliberately apply it.',
|
|
73365
|
+
inputSchema: {
|
|
73366
|
+
intentId: zod_1.z.string().optional().describe('Repository-authority intent to inspect. Omit to list workspace-wide.'),
|
|
73367
|
+
status: zod_1.z.enum(['open', 'applied', 'rejected', 'withdrawn', 'superseded', 'all']).optional().describe('Lifecycle filter; defaults to open'),
|
|
73368
|
+
},
|
|
73369
|
+
annotations: READ_ONLY,
|
|
73370
|
+
}, async ({ intentId, status }) => {
|
|
73371
|
+
const cloud = requireCloudClient();
|
|
73372
|
+
const result = intentId
|
|
73373
|
+
? await cloud.listIntentChangeRequests(intentId, status ?? 'open')
|
|
73374
|
+
: await cloud.listWorkspaceIntentChangeRequests(status ?? 'open');
|
|
73375
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
73376
|
+
});
|
|
73377
|
+
cloudOnly.registerTool('get_intent_change_request', {
|
|
73378
|
+
title: 'Get Intent Change Request',
|
|
73379
|
+
description: 'Read one structured PM request with its exact base revision, operation, expected old value, proposed value, and reason. Reading records agent consumption; it does not claim the request was applied.',
|
|
73380
|
+
inputSchema: {
|
|
73381
|
+
intentId: zod_1.z.string().describe('The request intent'),
|
|
73382
|
+
requestId: zod_1.z.string().describe('The change-request ID'),
|
|
73383
|
+
},
|
|
73384
|
+
annotations: READ_ONLY,
|
|
73385
|
+
}, async ({ intentId, requestId }) => {
|
|
73386
|
+
const result = await requireCloudClient().getIntentChangeRequest(intentId, requestId);
|
|
73387
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.changeRequest, null, 2) }] };
|
|
73388
|
+
});
|
|
73389
|
+
cloudOnly.registerTool('reject_intent_change_request', {
|
|
73390
|
+
title: 'Reject Intent Change Request',
|
|
73391
|
+
description: 'Report that an open PM request cannot or should not be applied. This records the API-key principal and requires a concrete reason. Do not use this for a stale base; re-read the current request instead.',
|
|
73392
|
+
inputSchema: {
|
|
73393
|
+
intentId: zod_1.z.string().describe('The request intent'),
|
|
73394
|
+
requestId: zod_1.z.string().describe('The change-request ID'),
|
|
73395
|
+
reason: zod_1.z.string().trim().min(1).describe('Why the repository agent cannot apply it'),
|
|
73396
|
+
},
|
|
73397
|
+
annotations: WRITE_OP,
|
|
73398
|
+
}, async ({ intentId, requestId, reason }) => {
|
|
73399
|
+
const result = await requireCloudClient().rejectIntentChangeRequest(intentId, requestId, reason);
|
|
73400
|
+
return {
|
|
73401
|
+
content: [{
|
|
73402
|
+
type: 'text',
|
|
73403
|
+
text: `Change request ${requestId} rejected: ${result.changeRequest.resolutionReason ?? reason}`,
|
|
73404
|
+
}],
|
|
73405
|
+
};
|
|
73406
|
+
});
|
|
73407
|
+
const outcomeInputShape = zod_1.z.union([
|
|
73408
|
+
zod_1.z.string().trim().min(1),
|
|
73409
|
+
zod_1.z.object({
|
|
73410
|
+
id: zod_1.z.string().trim().min(1).optional(),
|
|
73411
|
+
text: zod_1.z.string().trim().min(1),
|
|
73412
|
+
priority: zod_1.z.enum(['must', 'should', 'could']).optional(),
|
|
73413
|
+
/** Null is meaningful on update: deliberately remove the stored recipe. */
|
|
73414
|
+
measurement: measurement_schema_1.outcomeMeasurementDefinitionShape.nullable().optional(),
|
|
73415
|
+
}),
|
|
73416
|
+
]);
|
|
72476
73417
|
cloudOnly.registerTool('get_intent_relations', {
|
|
72477
73418
|
title: 'Get Intent Relations',
|
|
72478
73419
|
description: 'Get the dependency graph for a specific intent. Shows what it depends on, enables, or blocks.',
|
|
@@ -72839,11 +73780,34 @@ function startMcpServer() {
|
|
|
72839
73780
|
if (isLocalMode) {
|
|
72840
73781
|
return { content: [{ type: 'text', text: 'Agent prompts require cloud mode for full context generation.' }] };
|
|
72841
73782
|
}
|
|
72842
|
-
const
|
|
73783
|
+
const cloud = requireCloudClient();
|
|
73784
|
+
const [result, openRequests] = await Promise.all([
|
|
73785
|
+
cloud.getIntentPrompt(intentId, 'claude-code', mode || 'execute'),
|
|
73786
|
+
cloud.listIntentChangeRequests(intentId, 'open').catch(() => ({ changeRequests: [], count: 0 })),
|
|
73787
|
+
]);
|
|
73788
|
+
const requestBlock = openRequests.changeRequests.length > 0
|
|
73789
|
+
? [
|
|
73790
|
+
'# OPEN PM CHANGE REQUESTS — HANDLE BEFORE IMPLEMENTATION',
|
|
73791
|
+
'',
|
|
73792
|
+
'These requests are judgment against one exact intent.md revision. Read each request, update intent.md deliberately, then call intent_save with that request\'s exact `changeRequestId` and `baseRepoBodyRevision`. Do not start product implementation until the resulting revision is authorized. Reject an unworkable request with reject_intent_change_request and a concrete reason.',
|
|
73793
|
+
'',
|
|
73794
|
+
...openRequests.changeRequests.flatMap((request) => [
|
|
73795
|
+
`## ${request.id}`,
|
|
73796
|
+
`- Base revision: ${request.baseRepoBodyRevision}`,
|
|
73797
|
+
`- Requested change: ${request.operation} ${request.targetField}${request.targetItemId ? ` (${request.targetItemId})` : ''}`,
|
|
73798
|
+
`- Reason: ${request.reason}`,
|
|
73799
|
+
...(request.expectedOldValue !== undefined ? [`- Expected old value: ${JSON.stringify(request.expectedOldValue)}`] : []),
|
|
73800
|
+
...(request.proposedValue !== undefined ? [`- Proposed value: ${JSON.stringify(request.proposedValue)}`] : []),
|
|
73801
|
+
'',
|
|
73802
|
+
]),
|
|
73803
|
+
'---',
|
|
73804
|
+
'',
|
|
73805
|
+
].join('\n')
|
|
73806
|
+
: '';
|
|
72843
73807
|
return {
|
|
72844
73808
|
content: [{
|
|
72845
73809
|
type: 'text',
|
|
72846
|
-
text: result.prompt
|
|
73810
|
+
text: `${requestBlock}${result.prompt}`,
|
|
72847
73811
|
}]
|
|
72848
73812
|
};
|
|
72849
73813
|
});
|
|
@@ -73041,7 +74005,7 @@ function startMcpServer() {
|
|
|
73041
74005
|
objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
|
|
73042
74006
|
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.'),
|
|
73043
74007
|
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.'),
|
|
73044
|
-
outcomes: zod_1.z.array(
|
|
74008
|
+
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.'),
|
|
73045
74009
|
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Hard limits the implementation must respect'),
|
|
73046
74010
|
healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('What to monitor after shipping'),
|
|
73047
74011
|
edgeCases: zod_1.z.array(zod_1.z.object({
|
|
@@ -73082,7 +74046,7 @@ function startMcpServer() {
|
|
|
73082
74046
|
title: zod_1.z.string().optional().describe('New title'),
|
|
73083
74047
|
objective: zod_1.z.string().optional().describe('Updated objective'),
|
|
73084
74048
|
currentState: zod_1.z.string().optional().describe('Updated as-is behavior (how it works today). Pass an empty string to clear it.'),
|
|
73085
|
-
outcomes: zod_1.z.array(
|
|
74049
|
+
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.'),
|
|
73086
74050
|
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Replace all constraints'),
|
|
73087
74051
|
healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace all health metrics'),
|
|
73088
74052
|
edgeCases: zod_1.z.array(zod_1.z.object({
|
|
@@ -73263,7 +74227,7 @@ function startMcpServer() {
|
|
|
73263
74227
|
role: 'user',
|
|
73264
74228
|
content: {
|
|
73265
74229
|
type: 'text',
|
|
73266
|
-
text: `I need to implement intent ${intentId}. Please:\n1. Use
|
|
74230
|
+
text: `I need to implement intent ${intentId}. Please:\n1. Use get_agent_prompt to fetch the full execution prompt; it includes any open PM change requests\n2. If a change request is open, read it, apply it to intent.md, and call intent_save with its exact changeRequestId and baseRepoBodyRevision. If it cannot be applied, use reject_intent_change_request with a concrete reason\n3. After any requested spec change, STOP until a signed-in product owner authorizes that exact new revision. Do not implement from a pending or stale revision\n4. Use get_constitution to check for workspace constraints I must respect\n5. Review the authorized intent details and create an implementation plan\n6. Name the work so the merge can find it: branch \`intent/${intentId}\`, or put \`pathmode:${intentId}\` in the pull request body\n7. After implementation, use verify_implementation to AI-grade your work against the spec\n8. If this change goes through a pull request, STOP there — the merge grades the real diff and moves the intent to Shipped. Do NOT call update_intent_status: an intent already at shipped is skipped by the merge, so an early flip replaces a diff-backed verdict with an unverified one\n9. Only if there will be no pull request, use update_intent_status to mark it "shipped"\n10. Use log_implementation_note to document key technical decisions`,
|
|
73267
74231
|
},
|
|
73268
74232
|
}],
|
|
73269
74233
|
};
|
|
@@ -73313,7 +74277,7 @@ function startMcpServer() {
|
|
|
73313
74277
|
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.'),
|
|
73314
74278
|
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.'),
|
|
73315
74279
|
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.'),
|
|
73316
|
-
outcomes: zod_1.z.array(
|
|
74280
|
+
outcomes: zod_1.z.array(outcomeInputShape).describe('Observable, testable state changes. The structured form may carry an optional provider-neutral saved-query measurement recipe.'),
|
|
73317
74281
|
decisions: zod_1.z.array(zod_1.z.object({
|
|
73318
74282
|
choice: zod_1.z.string(),
|
|
73319
74283
|
ruledOut: zod_1.z.string().optional(),
|
|
@@ -73370,9 +74334,9 @@ function startMcpServer() {
|
|
|
73370
74334
|
}
|
|
73371
74335
|
server.registerTool('check_intent_readiness', {
|
|
73372
74336
|
title: 'Check Intent Readiness (Preflight)',
|
|
73373
|
-
description: 'Run the deterministic preflight on an intent spec before handing it to an implementation agent. Six calibrated checks — title, objective, outcomes, constraints, edge cases, verification — computed by pure functions: no model call, no network, the same spec always gets the same verdict. Pass a spec inline to check before saving, pass intentId to check a specific saved intent, or pass nothing to
|
|
74337
|
+
description: 'Run the deterministic preflight on an intent spec before handing it to an implementation agent. Six calibrated checks — title, objective, outcomes, constraints, edge cases, verification — computed by pure functions: no model call, no network, the same spec always gets the same verdict. Pass a spec inline to check before saving, pass intentId to check a specific saved intent, or pass nothing to resolve the repo-bound intent.md from MCP workspace roots or the launch directory. Cloud mode falls back to the workspace current intent only after declared roots show no repo file; when the repository cannot be identified it refuses to guess. A failing verdict names the exact blockers; repair the fields and re-run. This is the gate that runs at preflight.pathmode.io.',
|
|
73374
74338
|
inputSchema: {
|
|
73375
|
-
spec: zod_1.z.object(intentSpecSchema).optional().describe('Check this spec directly (before saving). When omitted,
|
|
74339
|
+
spec: zod_1.z.object(intentSpecSchema).optional().describe('Check this spec directly (before saving). When omitted, resolve the repo-bound intent.md from MCP roots or cwd. Cloud fallback is allowed only when declared roots are known to contain no intent.md; unresolved repositories are never guessed.'),
|
|
73376
74340
|
intentId: zod_1.z.string().optional().describe('Check a specific saved intent by id. Ignored when spec is passed.'),
|
|
73377
74341
|
},
|
|
73378
74342
|
annotations: READ_ONLY,
|
|
@@ -73384,9 +74348,18 @@ function startMcpServer() {
|
|
|
73384
74348
|
// "current" intent is only a heuristic, so it must not outrank the repo binding.
|
|
73385
74349
|
// An explicit different id still wins: the caller deliberately selected another
|
|
73386
74350
|
// saved intent rather than asking us to infer the current one.
|
|
73387
|
-
const
|
|
73388
|
-
if (
|
|
73389
|
-
|
|
74351
|
+
const repoResolution = await resolveRepoBoundIntent(intentId);
|
|
74352
|
+
if (repoResolution.kind === 'ambiguous') {
|
|
74353
|
+
return {
|
|
74354
|
+
content: [{
|
|
74355
|
+
type: 'text',
|
|
74356
|
+
text: `More than one MCP workspace root contains an intent.md (${repoResolution.paths.join(', ')}). Pass intentId or spec explicitly; Pathmode will not guess which repository governs this preflight.`,
|
|
74357
|
+
}],
|
|
74358
|
+
isError: true,
|
|
74359
|
+
};
|
|
74360
|
+
}
|
|
74361
|
+
if (repoResolution.kind === 'found') {
|
|
74362
|
+
subject = repoResolution.intent;
|
|
73390
74363
|
sourceNote = 'repo-bound intent.md';
|
|
73391
74364
|
}
|
|
73392
74365
|
else if (isLocalMode) {
|
|
@@ -73405,6 +74378,15 @@ function startMcpServer() {
|
|
|
73405
74378
|
}
|
|
73406
74379
|
}
|
|
73407
74380
|
else {
|
|
74381
|
+
if (!intentId && !repoResolution.rootsInspected) {
|
|
74382
|
+
return {
|
|
74383
|
+
content: [{
|
|
74384
|
+
type: 'text',
|
|
74385
|
+
text: 'Pathmode could not resolve this repository: the MCP client declared no usable workspace roots and the server launch directory has no intent.md. Pass intentId or spec explicitly, or launch the server from the repository root. The workspace current intent was not used because it may belong to a different repository.',
|
|
74386
|
+
}],
|
|
74387
|
+
isError: true,
|
|
74388
|
+
};
|
|
74389
|
+
}
|
|
73408
74390
|
const cloud = requireCloudClient();
|
|
73409
74391
|
// The list projection (INTENT_LIST_SELECT) omits intent_spec_edge_cases, so
|
|
73410
74392
|
// grading a listed row fails the edge-case gate unconditionally — no saved
|
|
@@ -73439,7 +74421,11 @@ function startMcpServer() {
|
|
|
73439
74421
|
spec: zod_1.z.object(intentSpecSchema),
|
|
73440
74422
|
path: zod_1.z.string().optional().describe('File path relative to the project root. Must stay inside the project (absolute paths and ".." are rejected). Defaults to intent.md'),
|
|
73441
74423
|
overwrite: zod_1.z.boolean().optional().describe('Replace the file even when it already holds a DIFFERENT intent. Default false — the save is refused instead, so an unrelated spec is never clobbered.'),
|
|
73442
|
-
|
|
74424
|
+
changeRequestId: zod_1.z.string().uuid().optional().describe('Structured PM request this save deliberately applies. Requires baseRepoBodyRevision.'),
|
|
74425
|
+
baseRepoBodyRevision: zod_1.z.string().regex(/^[a-f0-9]{64}$/i).optional().describe('Exact base revision carried by the change request. Requires changeRequestId.'),
|
|
74426
|
+
}, async ({ spec, path, overwrite, changeRequestId, baseRepoBodyRevision }) => performIntentSave(spec, path, overwrite, 'intent_save', changeRequestId || baseRepoBodyRevision
|
|
74427
|
+
? { id: changeRequestId, baseRepoBodyRevision }
|
|
74428
|
+
: undefined));
|
|
73443
74429
|
/**
|
|
73444
74430
|
* The whole save path — collision policy, verdict stamp, confirmation carry-forward, cloud push
|
|
73445
74431
|
* with identity settlement — extracted so intent_import can reuse it EXACTLY. Import must not be
|
|
@@ -73447,8 +74433,17 @@ function startMcpServer() {
|
|
|
73447
74433
|
* write path costs, and the confirmation carry-forward here is load-bearing (M1's tool boundary
|
|
73448
74434
|
* assumes every writer goes through this).
|
|
73449
74435
|
*/
|
|
73450
|
-
async function performIntentSave(spec, path, overwrite, retryTool = 'intent_save') {
|
|
74436
|
+
async function performIntentSave(spec, path, overwrite, retryTool = 'intent_save', changeRequest) {
|
|
73451
74437
|
{
|
|
74438
|
+
if (changeRequest && (!changeRequest.id || !changeRequest.baseRepoBodyRevision)) {
|
|
74439
|
+
return {
|
|
74440
|
+
content: [{
|
|
74441
|
+
type: 'text',
|
|
74442
|
+
text: '✗ changeRequestId and baseRepoBodyRevision are required together. Re-read the open request and pass both exact values.',
|
|
74443
|
+
}],
|
|
74444
|
+
isError: true,
|
|
74445
|
+
};
|
|
74446
|
+
}
|
|
73452
74447
|
const filePath = resolveWithinProject(path || 'intent.md');
|
|
73453
74448
|
const existing = (0, local_reader_1.readIntentMeta)(filePath);
|
|
73454
74449
|
const decision = (0, save_policy_1.decideSave)({
|
|
@@ -73471,6 +74466,15 @@ function startMcpServer() {
|
|
|
73471
74466
|
}],
|
|
73472
74467
|
};
|
|
73473
74468
|
}
|
|
74469
|
+
if (changeRequest && decision.action !== 'update') {
|
|
74470
|
+
return {
|
|
74471
|
+
content: [{
|
|
74472
|
+
type: 'text',
|
|
74473
|
+
text: '✗ A change request can only be applied from the already-connected intent.md that carries its cloud identity and specVersion. Pull the intent first, then retry.',
|
|
74474
|
+
}],
|
|
74475
|
+
isError: true,
|
|
74476
|
+
};
|
|
74477
|
+
}
|
|
73474
74478
|
const { id, version, status, created } = decision;
|
|
73475
74479
|
// The preflight verdict travels with the file: stamped into frontmatter on every save,
|
|
73476
74480
|
// recomputed from the spec alone (deterministic). A failing verdict never blocks the
|
|
@@ -73553,6 +74557,9 @@ function startMcpServer() {
|
|
|
73553
74557
|
},
|
|
73554
74558
|
decisions: spec.decisions,
|
|
73555
74559
|
implementationContext: spec.implementationContext,
|
|
74560
|
+
changeRequest: changeRequest?.id && changeRequest.baseRepoBodyRevision
|
|
74561
|
+
? { id: changeRequest.id, baseRepoBodyRevision: changeRequest.baseRepoBodyRevision }
|
|
74562
|
+
: undefined,
|
|
73556
74563
|
onIdentitySettled: writeSpecFile,
|
|
73557
74564
|
});
|
|
73558
74565
|
if (!result.ok) {
|