@agentcomms/resend 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_LICENSES +9 -9
- package/dist/cli.mjs +13 -5
- package/dist/{dist-COPefvtq-COGS2a-K.mjs → dist-C7DG2LdL-l0mj9_dH.mjs} +55 -33
- package/dist/index.d.mts +141 -33
- package/dist/index.mjs +2 -2
- package/dist/{mcp-DXXb3Vv3-kCxsVLO4.mjs → mcp-DIH4cS6P-CIWWZQbz.mjs} +181 -68
- package/dist/{send-DEor7f1N.mjs → send-BOMCKvLT.mjs} +1264 -228
- package/dist/{server-xhrQV9qo.mjs → server-CbcJL9CZ.mjs} +6 -6
- package/dist/{src-D_zzAWoS-BhK0GFdq-oo-8IJem.mjs → src-WCy6ifGf-DXrNj-15-D0oRtifb.mjs} +75 -41
- package/dist/{stdio-q4c3585j.mjs → stdio-B8UXEGiG.mjs} +28 -3
- package/dist/{stdio-CHZ2WRcx-B2_wGdA5.mjs → stdio-CyjIgrch-59zcjSEt.mjs} +9 -3
- package/dist/{stdio-entry-BKyy2XRF.mjs → stdio-entry-BfcIk24a.mjs} +1 -1
- package/package.json +5 -5
|
@@ -12,7 +12,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/version.ts
|
|
14
14
|
/** The package version, read from package.json at build time. */
|
|
15
|
-
const VERSION$1 = "0.
|
|
15
|
+
const VERSION$1 = "0.18.1";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/api/routes.ts
|
|
18
18
|
/**
|
|
@@ -201,7 +201,7 @@ const RESEND_CALLER = Object.freeze({
|
|
|
201
201
|
packageName: PACKAGE_NAME
|
|
202
202
|
});
|
|
203
203
|
//#endregion
|
|
204
|
-
//#region ../core/dist/update-check-
|
|
204
|
+
//#region ../core/dist/update-check-DvZxdFU_.mjs
|
|
205
205
|
/**
|
|
206
206
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
207
207
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -1386,6 +1386,24 @@ const CHANNEL_SNAPSHOT = [
|
|
|
1386
1386
|
}
|
|
1387
1387
|
}
|
|
1388
1388
|
];
|
|
1389
|
+
/** Every declared service server: what core knows about it without installing it. */
|
|
1390
|
+
const SERVICE_SNAPSHOT = [{
|
|
1391
|
+
packageName: "@agentcomms/events-daemon",
|
|
1392
|
+
declaration: {
|
|
1393
|
+
kind: "service",
|
|
1394
|
+
binary: "agent-events",
|
|
1395
|
+
server: {
|
|
1396
|
+
defaultName: "events",
|
|
1397
|
+
entry: "src/mcp/server.ts",
|
|
1398
|
+
factory: "createEventsMcpServer"
|
|
1399
|
+
},
|
|
1400
|
+
operations: "src/operations",
|
|
1401
|
+
skills: {
|
|
1402
|
+
prefix: "agentcomms-",
|
|
1403
|
+
contract: "skills/_shared/contract-events.md"
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}];
|
|
1389
1407
|
const DISCLOSURE_VERSION_KINDS = [
|
|
1390
1408
|
"rule",
|
|
1391
1409
|
"target",
|
|
@@ -1575,8 +1593,8 @@ function bindingDigestOf(record) {
|
|
|
1575
1593
|
* Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
|
|
1576
1594
|
* but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
|
|
1577
1595
|
* as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
|
|
1578
|
-
* PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A
|
|
1579
|
-
* has to be safe in both
|
|
1596
|
+
* PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A common line printed on
|
|
1597
|
+
* Windows has to be safe in both — and safe is not enough:
|
|
1580
1598
|
* the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
|
|
1581
1599
|
* hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
|
|
1582
1600
|
* reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
|
|
@@ -1603,15 +1621,17 @@ function bindingDigestOf(record) {
|
|
|
1603
1621
|
* backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
|
|
1604
1622
|
* backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
|
|
1605
1623
|
* which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
|
|
1606
|
-
* - With any other word in it,
|
|
1624
|
+
* - With any other word in it, the common renderer has no line, and the printers show the command's words
|
|
1607
1625
|
* as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
|
|
1608
1626
|
* still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
|
|
1609
1627
|
* hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
|
|
1610
1628
|
* as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
|
|
1611
1629
|
*
|
|
1612
|
-
* - The program itself has to be left as it is. Quoted, it is a string to PowerShell rather than a
|
|
1613
|
-
*
|
|
1614
|
-
* quotes,
|
|
1630
|
+
* - The program itself has to be left as it is in the common form. Quoted, it is a string to PowerShell rather than a
|
|
1631
|
+
* command. When its path needs quoting, CUE-410 may emit a labelled PowerShell line: `&`, then every word in single
|
|
1632
|
+
* quotes, with an inner single quote doubled. Empty words, trailing backslashes, double quotes and control or
|
|
1633
|
+
* formatting characters retain the inert-JSON fallback because their native-program boundary cannot be proved
|
|
1634
|
+
* across the supported PowerShell versions.
|
|
1615
1635
|
*
|
|
1616
1636
|
* Everywhere else there is always a line: single quotes make any word safe.
|
|
1617
1637
|
*
|
|
@@ -1627,9 +1647,23 @@ function quoteCommand(words, platform) {
|
|
|
1627
1647
|
};
|
|
1628
1648
|
const printed = words.map(windowsShellWord);
|
|
1629
1649
|
const programBare = words.length === 0 || printed[0] === words[0];
|
|
1650
|
+
if (!printed.includes(null) && programBare) return {
|
|
1651
|
+
words,
|
|
1652
|
+
line: printed.join(" "),
|
|
1653
|
+
platform
|
|
1654
|
+
};
|
|
1655
|
+
if (!programBare && printed.slice(1).every((word) => word !== null)) {
|
|
1656
|
+
const powershell = words.map(powerShellWord);
|
|
1657
|
+
if (!powershell.includes(null)) return {
|
|
1658
|
+
words,
|
|
1659
|
+
line: `& ${powershell.join(" ")}`,
|
|
1660
|
+
platform,
|
|
1661
|
+
shell: "powershell"
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1630
1664
|
return {
|
|
1631
1665
|
words,
|
|
1632
|
-
line:
|
|
1666
|
+
line: null,
|
|
1633
1667
|
platform
|
|
1634
1668
|
};
|
|
1635
1669
|
}
|
|
@@ -1642,6 +1676,14 @@ function quotedLineWithWordsToFill(command, ...toFill) {
|
|
|
1642
1676
|
if (command.line === null) return null;
|
|
1643
1677
|
const sentinel = command.words.indexOf("--");
|
|
1644
1678
|
if (sentinel < 0) return [command.line, ...toFill].join(" ");
|
|
1679
|
+
if (command.shell === "powershell") {
|
|
1680
|
+
const printed = command.words.map((each) => powerShellWord(each));
|
|
1681
|
+
return `& ${[
|
|
1682
|
+
...printed.slice(0, sentinel),
|
|
1683
|
+
...toFill,
|
|
1684
|
+
...printed.slice(sentinel)
|
|
1685
|
+
].join(" ")}`;
|
|
1686
|
+
}
|
|
1645
1687
|
const word = command.platform === "win32" ? windowsShellWord : posixShellWord;
|
|
1646
1688
|
const printed = command.words.map((each) => word(each));
|
|
1647
1689
|
return [
|
|
@@ -1669,6 +1711,11 @@ function windowsShellWord(word) {
|
|
|
1669
1711
|
if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
|
|
1670
1712
|
return `"${word}"`;
|
|
1671
1713
|
}
|
|
1714
|
+
/** One native-command word in a PowerShell single-quoted literal, or null when a one-line call is not provable. */
|
|
1715
|
+
function powerShellWord(word) {
|
|
1716
|
+
if (word === "" || word.endsWith("\\") || /["\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
|
|
1717
|
+
return `'${word.replaceAll("'", "''")}'`;
|
|
1718
|
+
}
|
|
1672
1719
|
/**
|
|
1673
1720
|
* The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
|
|
1674
1721
|
* parse, and nothing to run.
|
|
@@ -1699,17 +1746,19 @@ function commandAsJson(words) {
|
|
|
1699
1746
|
/** What is said beside a command shown as JSON. */
|
|
1700
1747
|
const TO_TYPE = "the command's words, written as JSON: one of them cannot be quoted the same way for cmd.exe and for PowerShell, so type the command yourself, with that word quoted for the shell you use";
|
|
1701
1748
|
/** What is said beside a command shown as JSON because its program's path needs quotes. */
|
|
1702
|
-
const TO_TYPE_PROGRAM = "the command's words, written as JSON: the program's path needs
|
|
1749
|
+
const TO_TYPE_PROGRAM = "the command's words, written as JSON: the program's path needs quoting and at least one word cannot be represented safely in a one-line PowerShell call, so type the command yourself, with each word quoted for the shell you use and, in PowerShell, `&` before the program";
|
|
1703
1750
|
function toType(command) {
|
|
1704
1751
|
return programNeedsQuotes(command) ? TO_TYPE_PROGRAM : TO_TYPE;
|
|
1705
1752
|
}
|
|
1706
1753
|
/** Quoted words in backticks, for a sentence — as words in JSON, saying it has to be typed, when there is no line. */
|
|
1707
1754
|
function inlineQuoted(command) {
|
|
1708
|
-
|
|
1755
|
+
if (command.line === null) return `\`${commandAsJson(command.words)}\` (${toType(command)})`;
|
|
1756
|
+
return `\`${command.line}\`${command.shell === "powershell" ? " (PowerShell)" : ""}`;
|
|
1709
1757
|
}
|
|
1710
1758
|
/** Quoted words as text of their own — a list's line, a field's value — or as words in JSON, saying to type them. */
|
|
1711
1759
|
function quotedText(command) {
|
|
1712
|
-
|
|
1760
|
+
if (command.line === null) return `${commandAsJson(command.words)} (${toType(command)})`;
|
|
1761
|
+
return command.shell === "powershell" ? `PowerShell:\n${command.line}` : command.line;
|
|
1713
1762
|
}
|
|
1714
1763
|
/**
|
|
1715
1764
|
* The same command with words the agent is to fill in — a placeholder such as `<folder>` — before its first `--`: for
|
|
@@ -2173,7 +2222,7 @@ function handoffSentence(handoff, say, options = {}) {
|
|
|
2173
2222
|
function handoffSentenceToFill(handoff, toFill, say, options = {}) {
|
|
2174
2223
|
if (!isCommand(handoff)) return noCommand(handoff, options);
|
|
2175
2224
|
const line = lineWithWordsToFill(handoff, ...toFill);
|
|
2176
|
-
if (line !== null) return say(`\`${line}
|
|
2225
|
+
if (line !== null) return say(`\`${line}\`${handoff.shell === "powershell" ? " (PowerShell)" : ""}`);
|
|
2177
2226
|
return say(inlineQuoted(quotedWithWordsToFill(handoff, toFill)));
|
|
2178
2227
|
}
|
|
2179
2228
|
/**
|
|
@@ -2393,7 +2442,7 @@ function nearestPackage(start) {
|
|
|
2393
2442
|
* Whether `path` is `root` or inside it, by whole path segments: `/x/core-evil` is not inside `/x/core`, which a
|
|
2394
2443
|
* comparison of strings would say it was. Both are taken as given; a caller compares real paths with real paths.
|
|
2395
2444
|
*/
|
|
2396
|
-
function isWithin(root, path) {
|
|
2445
|
+
function isWithin$1(root, path) {
|
|
2397
2446
|
const between = relative(resolve(root), resolve(path));
|
|
2398
2447
|
return between === "" || !isAbsolute(between) && between !== ".." && !/^\.\.[\\/]/.test(between);
|
|
2399
2448
|
}
|
|
@@ -2463,15 +2512,18 @@ var ExternalCommand = class {
|
|
|
2463
2512
|
#reason;
|
|
2464
2513
|
/** The words, as the program is to receive them. */
|
|
2465
2514
|
words;
|
|
2466
|
-
/** The line to paste; null when
|
|
2515
|
+
/** The line to paste; null when no supported common or labelled shell rendering is safe. */
|
|
2467
2516
|
line;
|
|
2468
2517
|
platform;
|
|
2518
|
+
shell;
|
|
2469
2519
|
constructor(gate, words, platform, reason) {
|
|
2470
2520
|
if (gate !== GATE$1) throw new TypeError("an external command is made by externalCommand, and only there");
|
|
2471
2521
|
this.#reason = reason;
|
|
2472
2522
|
this.words = Object.freeze([...words]);
|
|
2473
|
-
|
|
2523
|
+
const quoted = quoteCommand(this.words, platform);
|
|
2524
|
+
this.line = quoted.line;
|
|
2474
2525
|
this.platform = platform;
|
|
2526
|
+
if (quoted.shell !== void 0) this.shell = quoted.shell;
|
|
2475
2527
|
Object.freeze(this);
|
|
2476
2528
|
}
|
|
2477
2529
|
/** Why this program, which is not part of this suite, is one a person may be told to run. */
|
|
@@ -3185,6 +3237,343 @@ function commandLinePackage(command, args) {
|
|
|
3185
3237
|
const line = [command, ...withoutPathOptions(args).words].join(" ");
|
|
3186
3238
|
return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
|
|
3187
3239
|
}
|
|
3240
|
+
const RECEIPT_VERSION = 1;
|
|
3241
|
+
const RECEIPT_FILE = "managed-shims.json";
|
|
3242
|
+
const MUTATION_LOCK_FILE = "managed-shims.lock";
|
|
3243
|
+
const POSIX_SHIM_MODE = 493;
|
|
3244
|
+
const WINDOWS_SHIM_NOTICE = "Managed CLI shims are not offered on Windows yet because cmd.exe re-parses arguments forwarded through .cmd files, including shell metacharacters. Use the full-path command or the labelled PowerShell & line the result provides.";
|
|
3245
|
+
function platformOf(context) {
|
|
3246
|
+
return context.platform ?? process.platform;
|
|
3247
|
+
}
|
|
3248
|
+
/** Why the selected platform has no managed shim in this release. */
|
|
3249
|
+
function managedShimUnsupportedNotice(context = {}) {
|
|
3250
|
+
return platformOf(context) === "win32" ? WINDOWS_SHIM_NOTICE : "Managed CLI shims are offered only on macOS and Linux in this release. Use the full-path command the result provides.";
|
|
3251
|
+
}
|
|
3252
|
+
/** The per-user directory managed installs put their command shims in. */
|
|
3253
|
+
function managedShimDirectory(context) {
|
|
3254
|
+
if (!managedShimsSupported(context)) throw new CommsError("CONFIG", managedShimUnsupportedNotice(context));
|
|
3255
|
+
return join(homeDirectory(context.env), ".local", "bin");
|
|
3256
|
+
}
|
|
3257
|
+
/** Managed CLI shims are POSIX-only until Windows has an argument-forwarding form that does not re-parse input. */
|
|
3258
|
+
function managedShimsSupported(context = {}) {
|
|
3259
|
+
const platform = platformOf(context);
|
|
3260
|
+
return platform === "darwin" || platform === "linux";
|
|
3261
|
+
}
|
|
3262
|
+
/** A managed shim's name: one plain command word, never a path, so joining it to the directory stays inside it. */
|
|
3263
|
+
function isSafeBinary(binary) {
|
|
3264
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(binary);
|
|
3265
|
+
}
|
|
3266
|
+
function checkedBinary(binary) {
|
|
3267
|
+
if (!isSafeBinary(binary)) throw new CommsError("CONFIG", `${JSON.stringify(binary)} is not a safe managed CLI binary name`);
|
|
3268
|
+
return binary;
|
|
3269
|
+
}
|
|
3270
|
+
/** The exact path for one declared binary's managed shim. */
|
|
3271
|
+
function managedShimPath(context, binary) {
|
|
3272
|
+
return join(managedShimDirectory(context), checkedBinary(binary));
|
|
3273
|
+
}
|
|
3274
|
+
function receiptPath(context) {
|
|
3275
|
+
return join(context.dataDir, RECEIPT_FILE);
|
|
3276
|
+
}
|
|
3277
|
+
function emptyReceipt() {
|
|
3278
|
+
return {
|
|
3279
|
+
version: RECEIPT_VERSION,
|
|
3280
|
+
shims: {}
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
async function readReceipt(context) {
|
|
3284
|
+
let parsed;
|
|
3285
|
+
try {
|
|
3286
|
+
parsed = JSON.parse(await readFile(receiptPath(context), "utf8"));
|
|
3287
|
+
} catch (error) {
|
|
3288
|
+
return error.code === "ENOENT" ? emptyReceipt() : null;
|
|
3289
|
+
}
|
|
3290
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== RECEIPT_VERSION || typeof parsed.shims !== "object" || parsed.shims === null) return null;
|
|
3291
|
+
const shims = {};
|
|
3292
|
+
for (const [binary, value] of Object.entries(parsed.shims)) {
|
|
3293
|
+
if (!isSafeBinary(binary) || typeof value !== "object" || value === null || ![
|
|
3294
|
+
"path",
|
|
3295
|
+
"digest",
|
|
3296
|
+
"node",
|
|
3297
|
+
"entry"
|
|
3298
|
+
].every((key) => typeof value[key] === "string")) return null;
|
|
3299
|
+
shims[binary] = value;
|
|
3300
|
+
}
|
|
3301
|
+
return {
|
|
3302
|
+
version: RECEIPT_VERSION,
|
|
3303
|
+
shims
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
async function writeReceipt(context, receipt) {
|
|
3307
|
+
await writeFileAtomic(receiptPath(context), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
3308
|
+
}
|
|
3309
|
+
function sha256(bytes) {
|
|
3310
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
3311
|
+
}
|
|
3312
|
+
function posixWord(word) {
|
|
3313
|
+
return `'${word.replaceAll("'", `'"'"'`)}'`;
|
|
3314
|
+
}
|
|
3315
|
+
function shimSource(spec) {
|
|
3316
|
+
return `#!/bin/sh\n# managed CLI shim v1\nexec ${spec.command.map(posixWord).join(" ")} "$@"\n`;
|
|
3317
|
+
}
|
|
3318
|
+
async function pinShimDirectory(context, create) {
|
|
3319
|
+
const publicDirectory = managedShimDirectory(context);
|
|
3320
|
+
if (create) await mkdir(publicDirectory, {
|
|
3321
|
+
recursive: true,
|
|
3322
|
+
mode: 493
|
|
3323
|
+
});
|
|
3324
|
+
let resolved;
|
|
3325
|
+
try {
|
|
3326
|
+
resolved = await realpath(publicDirectory);
|
|
3327
|
+
} catch (error) {
|
|
3328
|
+
if (!create && error.code === "ENOENT") return null;
|
|
3329
|
+
throw error;
|
|
3330
|
+
}
|
|
3331
|
+
return {
|
|
3332
|
+
public: publicDirectory,
|
|
3333
|
+
resolved
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
async function assertPinnedDirectory(directory) {
|
|
3337
|
+
let now;
|
|
3338
|
+
try {
|
|
3339
|
+
now = await realpath(directory.public);
|
|
3340
|
+
} catch {
|
|
3341
|
+
throw new CommsError("CONFIG", `the managed shim directory's resolved directory changed from ${directory.resolved} during the operation`, { hint: "Nothing was changed. Inspect the directory link, then run the install, update or prune again." });
|
|
3342
|
+
}
|
|
3343
|
+
if (now !== directory.resolved) throw new CommsError("CONFIG", `the managed shim directory's resolved directory changed from ${directory.resolved} to ${now} during the operation`, { hint: "Nothing was changed. Inspect the directory link, then run the install, update or prune again." });
|
|
3344
|
+
}
|
|
3345
|
+
async function regularFileBytes(path) {
|
|
3346
|
+
try {
|
|
3347
|
+
if (!(await lstat(path)).isFile()) return null;
|
|
3348
|
+
return await readFile(path, "utf8");
|
|
3349
|
+
} catch (error) {
|
|
3350
|
+
if (error.code === "ENOENT") return null;
|
|
3351
|
+
throw error;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
async function pathExists(path) {
|
|
3355
|
+
try {
|
|
3356
|
+
await lstat(path);
|
|
3357
|
+
return true;
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
if (error.code === "ENOENT") return false;
|
|
3360
|
+
throw error;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
/** Serialize this installation's receipt and shim mutations across its processes. */
|
|
3364
|
+
async function withMutationLock(context, change) {
|
|
3365
|
+
await mkdir(context.dataDir, {
|
|
3366
|
+
recursive: true,
|
|
3367
|
+
mode: 448
|
|
3368
|
+
});
|
|
3369
|
+
return withFileLock(join(context.dataDir, MUTATION_LOCK_FILE), change, {
|
|
3370
|
+
timeoutMs: 5e3,
|
|
3371
|
+
staleMs: 3e4,
|
|
3372
|
+
renewMs: 1e4,
|
|
3373
|
+
...context.lockOptions
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
/** Publish complete shim bytes only if the target is still absent. */
|
|
3377
|
+
async function createShimExclusive(directory, path, source) {
|
|
3378
|
+
const nonce = randomBytes(6).toString("hex");
|
|
3379
|
+
const temp = join(directory.resolved, `.${basename(path)}.${nonce}.tmp`);
|
|
3380
|
+
await assertPinnedDirectory(directory);
|
|
3381
|
+
const handle = await open(temp, "wx", POSIX_SHIM_MODE);
|
|
3382
|
+
try {
|
|
3383
|
+
await handle.writeFile(source);
|
|
3384
|
+
await handle.sync();
|
|
3385
|
+
} finally {
|
|
3386
|
+
await handle.close();
|
|
3387
|
+
}
|
|
3388
|
+
await assertPinnedDirectory(directory);
|
|
3389
|
+
await chmod(temp, POSIX_SHIM_MODE);
|
|
3390
|
+
try {
|
|
3391
|
+
await assertPinnedDirectory(directory);
|
|
3392
|
+
await link(temp, path);
|
|
3393
|
+
} finally {
|
|
3394
|
+
await assertPinnedDirectory(directory);
|
|
3395
|
+
await rm(temp, { force: true });
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
/** Restore an object moved aside without ever replacing a raced object at its public path. */
|
|
3399
|
+
async function restoreMoved(directory, path, moved) {
|
|
3400
|
+
try {
|
|
3401
|
+
await assertPinnedDirectory(directory);
|
|
3402
|
+
await link(moved, path);
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
if (error.code === "EEXIST") return false;
|
|
3405
|
+
throw error;
|
|
3406
|
+
}
|
|
3407
|
+
await assertPinnedDirectory(directory);
|
|
3408
|
+
await rm(moved);
|
|
3409
|
+
return true;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Move the exact current object aside atomically, then prove that object still has the owned digest. A raced foreign
|
|
3413
|
+
* file is put back; if another object has already occupied its path, both are preserved and the change is refused.
|
|
3414
|
+
*/
|
|
3415
|
+
async function moveOwned(directory, path, digest, suffix, before) {
|
|
3416
|
+
await before?.(path);
|
|
3417
|
+
await assertPinnedDirectory(directory);
|
|
3418
|
+
const moved = join(directory.resolved, `.${basename(path)}.${randomBytes(6).toString("hex")}.${suffix}`);
|
|
3419
|
+
try {
|
|
3420
|
+
await rename(path, moved);
|
|
3421
|
+
} catch (error) {
|
|
3422
|
+
if (error.code === "ENOENT") return null;
|
|
3423
|
+
throw error;
|
|
3424
|
+
}
|
|
3425
|
+
const bytes = await regularFileBytes(moved);
|
|
3426
|
+
if (bytes !== null && sha256(bytes) === digest) return moved;
|
|
3427
|
+
if (!await restoreMoved(directory, path, moved)) throw new CommsError("CONFIG", `${path} changed during a managed shim operation; its bytes were preserved at ${moved}`, { hint: "Resolve the two files by hand, then run the install, update or prune again." });
|
|
3428
|
+
return null;
|
|
3429
|
+
}
|
|
3430
|
+
/** Remove only the exact object whose bytes prove it is still owned. */
|
|
3431
|
+
async function removeOwned(directory, path, digest, before) {
|
|
3432
|
+
const moved = await moveOwned(directory, path, digest, "remove", before);
|
|
3433
|
+
if (moved === null) return false;
|
|
3434
|
+
await assertPinnedDirectory(directory);
|
|
3435
|
+
await rm(moved);
|
|
3436
|
+
return true;
|
|
3437
|
+
}
|
|
3438
|
+
function normalPath(path, platform) {
|
|
3439
|
+
const trimmed = path.replace(/[\\/]+$/, "");
|
|
3440
|
+
return platform === "win32" ? trimmed.toLowerCase() : trimmed;
|
|
3441
|
+
}
|
|
3442
|
+
function directoryOnPath(context, directory) {
|
|
3443
|
+
const platform = platformOf(context);
|
|
3444
|
+
const value = context.env.PATH ?? context.env.Path ?? "";
|
|
3445
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
3446
|
+
return value.split(separator).filter(Boolean).some((part) => normalPath(part, platform) === normalPath(directory, platform));
|
|
3447
|
+
}
|
|
3448
|
+
function pathNotice(directory) {
|
|
3449
|
+
return `The shim directory ${directory} is not on PATH. For this shell, run: export PATH=${posixWord(directory)}:"$PATH". No shell profile was edited.`;
|
|
3450
|
+
}
|
|
3451
|
+
/** Create or repoint one shim, but only while its target is free or still matches this installation's receipt. */
|
|
3452
|
+
async function writeManagedShim(context, spec) {
|
|
3453
|
+
checkedBinary(spec.binary);
|
|
3454
|
+
const directory = await pinShimDirectory(context, true);
|
|
3455
|
+
if (directory === null) throw new CommsError("UNEXPECTED", "the managed shim directory was not created");
|
|
3456
|
+
const publicPath = join(directory.public, spec.binary);
|
|
3457
|
+
const path = join(directory.resolved, spec.binary);
|
|
3458
|
+
const result = (status) => {
|
|
3459
|
+
const onPath = directoryOnPath(context, directory.public);
|
|
3460
|
+
return {
|
|
3461
|
+
path: publicPath,
|
|
3462
|
+
directory: directory.public,
|
|
3463
|
+
status,
|
|
3464
|
+
onPath,
|
|
3465
|
+
...onPath ? {} : { pathNotice: pathNotice(directory.public) }
|
|
3466
|
+
};
|
|
3467
|
+
};
|
|
3468
|
+
return withMutationLock(context, async () => {
|
|
3469
|
+
const receipt = await readReceipt(context);
|
|
3470
|
+
const recorded = receipt?.shims[spec.binary];
|
|
3471
|
+
const current = await regularFileBytes(path);
|
|
3472
|
+
const owned = receipt !== null && recorded !== void 0 && recorded.path === path && current !== null && sha256(current) === recorded.digest;
|
|
3473
|
+
if (await pathExists(path) && !owned) return result("foreign");
|
|
3474
|
+
if (receipt === null || recorded !== void 0 && recorded.path !== path) return result("foreign");
|
|
3475
|
+
const node = spec.command[0];
|
|
3476
|
+
if (node === void 0) throw new CommsError("CONFIG", "the managed shim command has no program");
|
|
3477
|
+
const source = shimSource(spec);
|
|
3478
|
+
const digest = sha256(source);
|
|
3479
|
+
let previous = null;
|
|
3480
|
+
if (owned) {
|
|
3481
|
+
previous = await moveOwned(directory, path, recorded.digest, "old", context.hooks?.beforeReplace);
|
|
3482
|
+
if (previous === null) return result("foreign");
|
|
3483
|
+
}
|
|
3484
|
+
try {
|
|
3485
|
+
await createShimExclusive(directory, path, source);
|
|
3486
|
+
} catch (error) {
|
|
3487
|
+
if (previous !== null) {
|
|
3488
|
+
if (!await restoreMoved(directory, path, previous)) throw new CommsError("CONFIG", `${path} was occupied while its owned shim was being updated; the old shim remains at ${previous}`, {
|
|
3489
|
+
hint: "Resolve the two files by hand, then run the managed install again.",
|
|
3490
|
+
cause: error
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
if (error.code === "EEXIST") return result("foreign");
|
|
3494
|
+
throw error;
|
|
3495
|
+
}
|
|
3496
|
+
receipt.shims[spec.binary] = {
|
|
3497
|
+
path,
|
|
3498
|
+
digest,
|
|
3499
|
+
node,
|
|
3500
|
+
entry: spec.entry
|
|
3501
|
+
};
|
|
3502
|
+
try {
|
|
3503
|
+
await assertPinnedDirectory(directory);
|
|
3504
|
+
await writeReceipt(context, receipt);
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
const removed = await removeOwned(directory, path, digest);
|
|
3507
|
+
if (previous !== null && removed && !await restoreMoved(directory, path, previous)) throw new CommsError("CONFIG", `the shim receipt failed and the old shim could not be restored from ${previous}`, {
|
|
3508
|
+
hint: "Restore that file to the reported shim path, then run the managed install again.",
|
|
3509
|
+
cause: error
|
|
3510
|
+
});
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
3513
|
+
if (previous !== null) {
|
|
3514
|
+
await assertPinnedDirectory(directory);
|
|
3515
|
+
await rm(previous);
|
|
3516
|
+
}
|
|
3517
|
+
return result(owned ? "updated" : "created");
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
function isWithin(root, path) {
|
|
3521
|
+
const child = relative(root, path);
|
|
3522
|
+
return child === "" || !child.startsWith("..") && !isAbsolute(child);
|
|
3523
|
+
}
|
|
3524
|
+
/** Remove unchanged owned shims whose recorded entry is inside one of the runtimes prune removed. */
|
|
3525
|
+
async function removeManagedShimsForRuntimes(context, runtimes, options = {}) {
|
|
3526
|
+
if (!managedShimsSupported(context)) return {
|
|
3527
|
+
removed: [],
|
|
3528
|
+
kept: []
|
|
3529
|
+
};
|
|
3530
|
+
const directory = await pinShimDirectory(context, false);
|
|
3531
|
+
if (directory === null) return {
|
|
3532
|
+
removed: [],
|
|
3533
|
+
kept: []
|
|
3534
|
+
};
|
|
3535
|
+
return withMutationLock(context, async () => {
|
|
3536
|
+
const receipt = await readReceipt(context);
|
|
3537
|
+
if (receipt === null) return {
|
|
3538
|
+
removed: [],
|
|
3539
|
+
kept: []
|
|
3540
|
+
};
|
|
3541
|
+
const removed = [];
|
|
3542
|
+
const kept = [];
|
|
3543
|
+
let changed = false;
|
|
3544
|
+
for (const [binary, recorded] of Object.entries(receipt.shims)) {
|
|
3545
|
+
if (!runtimes.some((runtime) => isWithin(runtime, recorded.entry))) continue;
|
|
3546
|
+
const publicPath = join(directory.public, binary);
|
|
3547
|
+
const path = join(directory.resolved, binary);
|
|
3548
|
+
if (recorded.path !== path) {
|
|
3549
|
+
kept.push(recorded.path);
|
|
3550
|
+
continue;
|
|
3551
|
+
}
|
|
3552
|
+
if (options.dryRun) {
|
|
3553
|
+
const bytes = await regularFileBytes(path);
|
|
3554
|
+
if (bytes !== null && sha256(bytes) === recorded.digest) removed.push(publicPath);
|
|
3555
|
+
else if (await pathExists(path)) kept.push(publicPath);
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if (await removeOwned(directory, path, recorded.digest, context.hooks?.beforeRemove)) {
|
|
3559
|
+
delete receipt.shims[binary];
|
|
3560
|
+
changed = true;
|
|
3561
|
+
removed.push(publicPath);
|
|
3562
|
+
} else if (!await pathExists(path)) {
|
|
3563
|
+
delete receipt.shims[binary];
|
|
3564
|
+
changed = true;
|
|
3565
|
+
} else kept.push(publicPath);
|
|
3566
|
+
}
|
|
3567
|
+
if (changed) {
|
|
3568
|
+
await assertPinnedDirectory(directory);
|
|
3569
|
+
await writeReceipt(context, receipt);
|
|
3570
|
+
}
|
|
3571
|
+
return {
|
|
3572
|
+
removed,
|
|
3573
|
+
kept
|
|
3574
|
+
};
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3188
3577
|
/**
|
|
3189
3578
|
* The environment a child process is started with: `env` as it is, and on Windows also
|
|
3190
3579
|
* `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
|
|
@@ -4269,8 +4658,8 @@ async function mergeIntoJsonConfig(path, client, name, entry) {
|
|
|
4269
4658
|
}
|
|
4270
4659
|
/** Starts the server exactly as a client would, and completes the handshake. */
|
|
4271
4660
|
async function verifyEntry(entry, product) {
|
|
4272
|
-
const { Client } = await import("./dist-
|
|
4273
|
-
const { StdioClientTransport } = await import("./stdio-
|
|
4661
|
+
const { Client } = await import("./dist-C7DG2LdL-l0mj9_dH.mjs");
|
|
4662
|
+
const { StdioClientTransport } = await import("./stdio-CyjIgrch-59zcjSEt.mjs");
|
|
4274
4663
|
const client = new Client({
|
|
4275
4664
|
name: `${product.binary}-install-check`,
|
|
4276
4665
|
version: product.version
|
|
@@ -4325,6 +4714,14 @@ function commandLinesFrom(ps) {
|
|
|
4325
4714
|
});
|
|
4326
4715
|
}
|
|
4327
4716
|
const RUNTIME_NAME = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]*)?$/;
|
|
4717
|
+
/** Whether a runtime's own manifest, the one the installer writes, names this package as what it installs. */
|
|
4718
|
+
async function pinsPackage(runtime, packageName) {
|
|
4719
|
+
try {
|
|
4720
|
+
return typeof JSON.parse(await readFile(join(runtime, "package.json"), "utf8")).dependencies?.[packageName] === "string";
|
|
4721
|
+
} catch {
|
|
4722
|
+
return false;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4328
4725
|
/**
|
|
4329
4726
|
* This product's managed runtimes on disk, oldest name first: every directory directly inside `<data>/runtime`,
|
|
4330
4727
|
* named like a version, that holds this product's package — never a symbolic link, never another product's runtime,
|
|
@@ -4353,6 +4750,7 @@ async function listManagedRuntimes(dataDir, packageName) {
|
|
|
4353
4750
|
} catch {
|
|
4354
4751
|
continue;
|
|
4355
4752
|
}
|
|
4753
|
+
if (!name.endsWith(suffix) && !await pinsPackage(path, packageName)) continue;
|
|
4356
4754
|
found.push({
|
|
4357
4755
|
path,
|
|
4358
4756
|
version: name.endsWith(suffix) ? name.slice(0, -suffix.length) : name
|
|
@@ -4395,7 +4793,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
|
|
|
4395
4793
|
runtimeDir: join(context.core.paths.dataDir, "runtime"),
|
|
4396
4794
|
dryRun: options.dryRun === true,
|
|
4397
4795
|
removed: [],
|
|
4398
|
-
kept: []
|
|
4796
|
+
kept: [],
|
|
4797
|
+
shims: {
|
|
4798
|
+
removed: [],
|
|
4799
|
+
kept: []
|
|
4800
|
+
}
|
|
4399
4801
|
};
|
|
4400
4802
|
const candidates = [];
|
|
4401
4803
|
for (const { path, version } of await listManagedRuntimes(context.core.paths.dataDir, product.packageName)) {
|
|
@@ -4462,6 +4864,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
|
|
|
4462
4864
|
const left = handedOut.filter((record) => !gone.some((path) => mentions([path], record.runtime)));
|
|
4463
4865
|
if (left.length !== handedOut.length) await writeFileAtomic(ledger, left.map((record) => `${JSON.stringify(record)}\n`).join(""));
|
|
4464
4866
|
}
|
|
4867
|
+
if (result.removed.length > 0) result.shims = await removeManagedShimsForRuntimes({
|
|
4868
|
+
env: context.env,
|
|
4869
|
+
dataDir: context.core.paths.dataDir,
|
|
4870
|
+
platform: context.platform
|
|
4871
|
+
}, result.removed.map((item) => item.path), { dryRun: result.dryRun });
|
|
4465
4872
|
return result;
|
|
4466
4873
|
}
|
|
4467
4874
|
/** Every runtime the installer handed out, or null when the record is there and cannot be read. */
|
|
@@ -4594,6 +5001,8 @@ function rivalWordWarnings(servers, word, can, product) {
|
|
|
4594
5001
|
}
|
|
4595
5002
|
/** Every channel in core's snapshot, the core first. */
|
|
4596
5003
|
const CHANNELS = Object.freeze(CHANNEL_SNAPSHOT.map((entry) => entry.manifest.channel));
|
|
5004
|
+
/** Every declared MCP server, channels first and then services, with no separate hand-kept list. */
|
|
5005
|
+
const SERVERS = Object.freeze([...CHANNELS, ...SERVICE_SNAPSHOT.map((entry) => entry.declaration.server.defaultName)]);
|
|
4597
5006
|
/**
|
|
4598
5007
|
* The value a pin's flag carries in an entry's arguments: `--account acme/resend`, or `--account=acme/resend` — the
|
|
4599
5008
|
* first given, in either form.
|
|
@@ -4654,8 +5063,28 @@ function serverFactsOf({ packageName, manifest }) {
|
|
|
4654
5063
|
};
|
|
4655
5064
|
}
|
|
4656
5065
|
const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
|
|
5066
|
+
/** Service server facts, derived from each service package's declaration. */
|
|
5067
|
+
const SERVICE_SERVERS = Object.freeze(Object.fromEntries(SERVICE_SNAPSHOT.map(({ packageName, declaration }) => [declaration.server.defaultName, {
|
|
5068
|
+
packageName,
|
|
5069
|
+
binary: declaration.binary,
|
|
5070
|
+
defaultServerName: declaration.server.defaultName,
|
|
5071
|
+
npxPackage: packageName,
|
|
5072
|
+
npxArgs: ["mcp"],
|
|
5073
|
+
serverArgs: () => [],
|
|
5074
|
+
narrowingOf: () => ({})
|
|
5075
|
+
}])));
|
|
5076
|
+
/** All managed servers, derived from channel manifests and service declarations. */
|
|
5077
|
+
const SERVERS_BY_ID = Object.freeze({
|
|
5078
|
+
...CHANNEL_SERVERS,
|
|
5079
|
+
...SERVICE_SERVERS
|
|
5080
|
+
});
|
|
4657
5081
|
/** How each server is named to a person: in a preview, and in what a tool returns. */
|
|
4658
5082
|
const CHANNEL_LABELS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, entry.manifest.label])));
|
|
5083
|
+
/** Labels for every managed server; a service declares its public server name. */
|
|
5084
|
+
const SERVER_LABELS = Object.freeze({
|
|
5085
|
+
...CHANNEL_LABELS,
|
|
5086
|
+
...Object.fromEntries(SERVICE_SNAPSHOT.map(({ declaration }) => [declaration.server.defaultName, declaration.server.defaultName]))
|
|
5087
|
+
});
|
|
4659
5088
|
/** A channel's manifest, from core's snapshot. */
|
|
4660
5089
|
function channelManifest(channel) {
|
|
4661
5090
|
return CHANNEL_SNAPSHOT.find((entry) => entry.manifest.channel === channel)?.manifest;
|
|
@@ -4663,10 +5092,18 @@ function channelManifest(channel) {
|
|
|
4663
5092
|
function isChannel(value) {
|
|
4664
5093
|
return typeof value === "string" && CHANNELS.includes(value);
|
|
4665
5094
|
}
|
|
5095
|
+
/** Whether a word identifies any channel or service server in this release. */
|
|
5096
|
+
function isServer(value) {
|
|
5097
|
+
return typeof value === "string" && SERVERS.includes(value);
|
|
5098
|
+
}
|
|
4666
5099
|
/** Refuses a word that is not a channel of this release, naming the ones that are. */
|
|
4667
5100
|
function notAChannel(value) {
|
|
4668
5101
|
return new CommsError("USAGE", `"${String(value)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
4669
5102
|
}
|
|
5103
|
+
/** Refuses a word that is not a declared server of this release. */
|
|
5104
|
+
function notAServer(value) {
|
|
5105
|
+
return new CommsError("USAGE", `"${String(value)}" is not a server`, { hint: `One of: ${SERVERS.join(", ")}.` });
|
|
5106
|
+
}
|
|
4670
5107
|
/** A channel's server facts, by its word; refused for a word that is not a channel. */
|
|
4671
5108
|
function channelServer(channel) {
|
|
4672
5109
|
if (!isChannel(channel)) throw notAChannel(channel);
|
|
@@ -4677,6 +5114,16 @@ function channelLabel(channel) {
|
|
|
4677
5114
|
if (!isChannel(channel)) throw notAChannel(channel);
|
|
4678
5115
|
return CHANNEL_LABELS[channel];
|
|
4679
5116
|
}
|
|
5117
|
+
/** A channel or service's server facts, by its declared server name. */
|
|
5118
|
+
function serverFacts(server) {
|
|
5119
|
+
if (!isServer(server)) throw notAServer(server);
|
|
5120
|
+
return SERVERS_BY_ID[server];
|
|
5121
|
+
}
|
|
5122
|
+
/** How a channel or service server is named to a person. */
|
|
5123
|
+
function serverLabel(server) {
|
|
5124
|
+
if (!isServer(server)) throw notAServer(server);
|
|
5125
|
+
return SERVER_LABELS[server];
|
|
5126
|
+
}
|
|
4680
5127
|
/** A channel's manifest, by its word; refused for a word that is not a channel. */
|
|
4681
5128
|
function requireChannelManifest(channel) {
|
|
4682
5129
|
const manifest = channelManifest(channel);
|
|
@@ -9643,7 +10090,7 @@ function folding(platform) {
|
|
|
9643
10090
|
return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
|
|
9644
10091
|
}
|
|
9645
10092
|
/** The package version, read from package.json at build time. */
|
|
9646
|
-
const VERSION = "0.
|
|
10093
|
+
const VERSION = "0.18.1";
|
|
9647
10094
|
/**
|
|
9648
10095
|
* Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
|
|
9649
10096
|
* to decide whether the release it last heard of is newer than the one running.
|
|
@@ -9779,15 +10226,18 @@ var PrintedCommand = class {
|
|
|
9779
10226
|
#entry;
|
|
9780
10227
|
/** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
|
|
9781
10228
|
words;
|
|
9782
|
-
/** The line to paste; null when
|
|
10229
|
+
/** The line to paste; null when no supported common or labelled shell rendering is safe. */
|
|
9783
10230
|
line;
|
|
9784
10231
|
platform;
|
|
10232
|
+
shell;
|
|
9785
10233
|
constructor(gate, words, platform, entry) {
|
|
9786
10234
|
if (gate !== GATE) throw new TypeError("a printed command is made by locateCliCommand, and only there");
|
|
9787
10235
|
this.#entry = entry;
|
|
9788
10236
|
this.words = Object.freeze([...words]);
|
|
9789
|
-
|
|
10237
|
+
const quoted = quoteCommand(this.words, platform);
|
|
10238
|
+
this.line = quoted.line;
|
|
9790
10239
|
this.platform = platform;
|
|
10240
|
+
if (quoted.shell !== void 0) this.shell = quoted.shell;
|
|
9791
10241
|
Object.freeze(this);
|
|
9792
10242
|
}
|
|
9793
10243
|
/**
|
|
@@ -9814,7 +10264,7 @@ const STRIP_TYPES = "--experimental-strip-types";
|
|
|
9814
10264
|
const TRANSFORM_TYPES = "--experimental-transform-types";
|
|
9815
10265
|
/** The suite's packages, by name, and its channel packages, which reach core through their own dependency on it. */
|
|
9816
10266
|
const SUITE_PACKAGES = new Set(CHANNEL_SNAPSHOT.map(({ packageName }) => packageName));
|
|
9817
|
-
const CHANNEL_PACKAGES
|
|
10267
|
+
const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
|
|
9818
10268
|
/**
|
|
9819
10269
|
* Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
|
|
9820
10270
|
*
|
|
@@ -9897,7 +10347,7 @@ function locateCliCommand(request, runtime = {
|
|
|
9897
10347
|
sameCheckout: true
|
|
9898
10348
|
});
|
|
9899
10349
|
}
|
|
9900
|
-
if (target.manifest.channel === "core" && CHANNEL_PACKAGES
|
|
10350
|
+
if (target.manifest.channel === "core" && CHANNEL_PACKAGES.has(caller.packageName)) {
|
|
9901
10351
|
const installed = installedPackage(dirname(callerFile), target.packageName);
|
|
9902
10352
|
if (installed === null) return fail("core", `${caller.packageName} has no ${target.packageName} installed where it can find one`);
|
|
9903
10353
|
const manifest = readPackageManifest(installed);
|
|
@@ -9925,7 +10375,9 @@ function locateCliCommand(request, runtime = {
|
|
|
9925
10375
|
}
|
|
9926
10376
|
if (!SUITE_PACKAGES.has(caller.packageName)) return fail("direction", `${caller.packageName} is not a package of this suite`);
|
|
9927
10377
|
if (request.registrations === void 0) return fail("no-registrations", "nothing registered was given to look through for another product's command");
|
|
9928
|
-
const
|
|
10378
|
+
const channelTarget = CHANNEL_SNAPSHOT.find((entry) => entry.packageName === target.packageName);
|
|
10379
|
+
if (channelTarget === void 0) return fail("direction", `${target.packageName} is not a channel package this locator can find through a registration`);
|
|
10380
|
+
const facts = registrationFactsOf(channelTarget);
|
|
9929
10381
|
const notUsed = [];
|
|
9930
10382
|
const usable = [];
|
|
9931
10383
|
let engineRange;
|
|
@@ -10023,8 +10475,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
|
|
|
10023
10475
|
let entry;
|
|
10024
10476
|
if (found.source) entry = join(found.root, "src", "cli.ts");
|
|
10025
10477
|
else {
|
|
10026
|
-
const
|
|
10027
|
-
const binary = typeof declared === "object" && declared !== null ? declared.binary : void 0;
|
|
10478
|
+
const binary = declaredBinary(found.manifest);
|
|
10028
10479
|
const bin = found.manifest.bin;
|
|
10029
10480
|
const path = binary === target.manifest.binary && typeof bin === "object" && bin !== null && Object.hasOwn(bin, target.manifest.binary) ? bin[target.manifest.binary] : void 0;
|
|
10030
10481
|
if (typeof path !== "string" || path.length === 0) return {
|
|
@@ -10053,6 +10504,16 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
|
|
|
10053
10504
|
};
|
|
10054
10505
|
}
|
|
10055
10506
|
/**
|
|
10507
|
+
* The command a package declares for itself: a channel's in its `agentcomms` field, a service's in `agentcommsPackage`
|
|
10508
|
+
* (D14). A library declares none, and no package declares both: the registry refuses one that does.
|
|
10509
|
+
*/
|
|
10510
|
+
function declaredBinary(manifest) {
|
|
10511
|
+
const channel = manifest.agentcomms;
|
|
10512
|
+
if (typeof channel === "object" && channel !== null) return channel.binary;
|
|
10513
|
+
const service = manifest.agentcommsPackage;
|
|
10514
|
+
if (typeof service === "object" && service !== null && service.kind === "service") return service.binary;
|
|
10515
|
+
}
|
|
10516
|
+
/**
|
|
10056
10517
|
* The target's package a registration's files lead to, or null: a script it starts, real or as written when it is
|
|
10057
10518
|
* gone, inside the package (or inside the product's own server wrapper, which depends on it); or, for the product's
|
|
10058
10519
|
* own command, the package that command links into or that npm installed beside it.
|
|
@@ -10184,7 +10645,7 @@ function notLocated(target, version, reason, detail, nodeVersion, extra) {
|
|
|
10184
10645
|
* anywhere else for the product, PATH included, and a later locate checks the path again.
|
|
10185
10646
|
*/
|
|
10186
10647
|
function checkedEntry(root, entry) {
|
|
10187
|
-
if (!isWithin(root, entry)) return {
|
|
10648
|
+
if (!isWithin$1(root, entry)) return {
|
|
10188
10649
|
ok: false,
|
|
10189
10650
|
why: `${entry} is outside its package, ${root}`
|
|
10190
10651
|
};
|
|
@@ -10197,7 +10658,7 @@ function checkedEntry(root, entry) {
|
|
|
10197
10658
|
why: `${entry} is not there`
|
|
10198
10659
|
};
|
|
10199
10660
|
}
|
|
10200
|
-
if (!isWithin(root, real)) return {
|
|
10661
|
+
if (!isWithin$1(root, real)) return {
|
|
10201
10662
|
ok: false,
|
|
10202
10663
|
why: `${entry} leads to ${real}, outside its package, ${root}`
|
|
10203
10664
|
};
|
|
@@ -10611,6 +11072,10 @@ function renderInstall(result, color) {
|
|
|
10611
11072
|
lines.push(paint(color, "bold", `Add this to ${result.configPath ?? `the MCP configuration of ${result.client}`}:`), result.snippet.trimEnd());
|
|
10612
11073
|
}
|
|
10613
11074
|
if (result.backupPath) lines.push(`The entry it replaced was saved to ${result.backupPath}.`);
|
|
11075
|
+
if (result.shim && result.shim.status !== "foreign") lines.push(`CLI shim ${result.shim.status}: ${result.shim.path}.`);
|
|
11076
|
+
if (result.shimFailure) lines.push(paint(color, "yellow", `CLI shim failed: ${result.shimFailure}.`));
|
|
11077
|
+
if (result.shimNotice) lines.push(paint(color, "yellow", result.shimNotice));
|
|
11078
|
+
if (result.shim?.pathNotice) lines.push(result.shim.pathNotice);
|
|
10614
11079
|
if (result.verification === "passed") lines.push(paint(color, "green", `Checked: ${result.verifyDetail}`));
|
|
10615
11080
|
else if (result.verification === "failed") {
|
|
10616
11081
|
lines.push(paint(color, "red", `Failed to start: ${result.verifyDetail ?? "no reason given"}`));
|
|
@@ -10625,6 +11090,8 @@ function renderPrune(result, color) {
|
|
|
10625
11090
|
if (result.refused) lines.push(paint(color, "yellow", `Nothing removed: ${result.refused}.`));
|
|
10626
11091
|
const verb = result.dryRun ? "Would remove" : "Removed";
|
|
10627
11092
|
for (const item of result.removed) lines.push(paint(color, "green", `${verb} ${item.version}: ${item.path}`));
|
|
11093
|
+
for (const path of result.shims.removed) lines.push(paint(color, "green", `${verb} CLI shim: ${path}`));
|
|
11094
|
+
for (const path of result.shims.kept) lines.push(`Kept foreign CLI shim: ${path}`);
|
|
10628
11095
|
for (const item of result.kept) lines.push(`Kept ${item.version} (${item.reason}): ${item.path}`);
|
|
10629
11096
|
if (lines.length === 0) lines.push(`No managed runtimes to remove in ${result.runtimeDir}.`);
|
|
10630
11097
|
return lines.join("\n");
|
|
@@ -10636,7 +11103,7 @@ function describeItem(item) {
|
|
|
10636
11103
|
const where = item.scope === "project" ? `for a project, in ${item.path}` : `in ${item.path}`;
|
|
10637
11104
|
const pins = item.narrowing.length > 0 ? `, ${item.narrowing.join(" ")}` : "";
|
|
10638
11105
|
const version = item.version === null ? "pins no release" : `${item.version}${item.version === item.latest ? "" : ` → ${item.latest}`}`;
|
|
10639
|
-
return `${
|
|
11106
|
+
return `${serverLabel(item.channel)} with ${item.client} as "${item.name}" ${where} (${item.launcher}, ${version}${pins})`;
|
|
10640
11107
|
}
|
|
10641
11108
|
function renderUpdate(result, color) {
|
|
10642
11109
|
const lines = [];
|
|
@@ -10648,6 +11115,8 @@ function renderUpdate(result, color) {
|
|
|
10648
11115
|
if (step.verification === "passed") lines.push(` Checked: ${step.detail}`);
|
|
10649
11116
|
else if (step.verification === "failed") lines.push(paint(color, "red", ` Failed to start: ${step.detail}`));
|
|
10650
11117
|
else lines.push(paint(color, "yellow", ` Not checked: ${step.detail ?? "skipped"}`));
|
|
11118
|
+
if (step.shim && step.shim.status !== "foreign") lines.push(` CLI shim ${step.shim.status}: ${step.shim.path}.`);
|
|
11119
|
+
if (step.shim?.pathNotice) lines.push(` ${step.shim.pathNotice}`);
|
|
10651
11120
|
for (const warning of step.warnings ?? []) lines.push(` ${warning}`);
|
|
10652
11121
|
} else lines.push(paint(color, "red", `${step.outcome === "skipped" ? "Skipped" : "Could not register"} ${what}: ${step.detail}`));
|
|
10653
11122
|
} else lines.push(step.outcome === "updated" ? paint(color, "green", `Updated the global ${step.package} from ${step.from} to ${step.to}.`) : paint(color, "red", `Could not update the global ${step.package}: ${step.detail ?? "no reason given"}`));
|
|
@@ -12052,10 +12521,10 @@ async function npmInstallGlobal(env, spec) {
|
|
|
12052
12521
|
}
|
|
12053
12522
|
}
|
|
12054
12523
|
/**
|
|
12055
|
-
* Registering, pruning and listing
|
|
12524
|
+
* Registering, pruning and listing every declared channel or service MCP server, from the core.
|
|
12056
12525
|
*
|
|
12057
|
-
* `agentcomms mcp install` registers
|
|
12058
|
-
*
|
|
12526
|
+
* `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
|
|
12527
|
+
* server; and channel CLIs register their own. They are the one change here, over
|
|
12059
12528
|
* the shared installer (`mcp-install.ts`), and all go through the change flow: registering a server is a loosening in
|
|
12060
12529
|
* the design's terms (§3.1) — it hands a client a new set of tools — and so is asked for once and applied on the
|
|
12061
12530
|
* second call with the approval. Printing an entry registers nothing and asks nobody.
|
|
@@ -12066,7 +12535,7 @@ async function npmInstallGlobal(env, spec) {
|
|
|
12066
12535
|
* the other — or worse, a channel's own command would register with no approval at all, which is what it did.
|
|
12067
12536
|
*
|
|
12068
12537
|
* The version installed is this core's own, or the calling channel's when it passes its product. The packages are
|
|
12069
|
-
* released together, at one version, so the core
|
|
12538
|
+
* released together, at one version, so the core asked to install Gmail or events installs its own release.
|
|
12070
12539
|
*/
|
|
12071
12540
|
const CLIENTS = Object.freeze([
|
|
12072
12541
|
"claude-code",
|
|
@@ -12107,10 +12576,10 @@ const exists = (path) => access(path, constants.R_OK).then(() => true, () => fal
|
|
|
12107
12576
|
* core depends on none of them. So the channel is looked for beside this core — `packages/gmail` next to
|
|
12108
12577
|
* `packages/core` in a checkout — and from source when this core runs from source, as the channel CLIs do.
|
|
12109
12578
|
*/
|
|
12110
|
-
async function localModuleUrl(
|
|
12579
|
+
async function localModuleUrl(server) {
|
|
12111
12580
|
const coreRoot = await corePackageRoot();
|
|
12112
|
-
const unscopedName =
|
|
12113
|
-
const root =
|
|
12581
|
+
const unscopedName = serverFacts(server).packageName.split("/").at(-1) ?? server;
|
|
12582
|
+
const root = server === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
|
|
12114
12583
|
const source = join(root, "src", "cli.ts");
|
|
12115
12584
|
const built = join(root, "dist", "cli.mjs");
|
|
12116
12585
|
const fromSource = fileURLToPath(import.meta.url).endsWith(".ts");
|
|
@@ -12118,7 +12587,7 @@ async function localModuleUrl(channel) {
|
|
|
12118
12587
|
if (!await exists(cli)) continue;
|
|
12119
12588
|
return pathToFileURL(cli === source ? join(root, "src", "mcp", "module.ts") : join(root, "dist", "module.mjs")).href;
|
|
12120
12589
|
}
|
|
12121
|
-
throw new CommsError("USAGE", `\`--launcher local\` registers a checkout's own code, and there is no ${
|
|
12590
|
+
throw new CommsError("USAGE", `\`--launcher local\` registers a checkout's own code, and there is no ${serverFacts(server).packageName} beside this one`, { hint: `Looked in ${root}. Use the managed launcher, or run the install from that checkout.` });
|
|
12122
12591
|
}
|
|
12123
12592
|
/**
|
|
12124
12593
|
* The product the shared installer registers, for one channel: at this core's version unless another is named.
|
|
@@ -12128,11 +12597,11 @@ async function localModuleUrl(channel) {
|
|
|
12128
12597
|
* newer release here is all it takes to register one this core is older than — which is what `agentcomms update` does.
|
|
12129
12598
|
* `comms_server_install` and every `mcp install` name none, and register their own release as they always have.
|
|
12130
12599
|
*/
|
|
12131
|
-
async function
|
|
12600
|
+
async function serverProduct(server, launcher, version = VERSION) {
|
|
12132
12601
|
return {
|
|
12133
|
-
...
|
|
12602
|
+
...serverFacts(server),
|
|
12134
12603
|
version,
|
|
12135
|
-
moduleUrl: launcher === "local" ? await localModuleUrl(
|
|
12604
|
+
moduleUrl: launcher === "local" ? await localModuleUrl(server) : ""
|
|
12136
12605
|
};
|
|
12137
12606
|
}
|
|
12138
12607
|
/**
|
|
@@ -12145,8 +12614,8 @@ async function channelProduct(channel, launcher, version = VERSION) {
|
|
|
12145
12614
|
* registered, from the channel; a product for another package would register something else under those words, and
|
|
12146
12615
|
* the person would have approved a sentence that was not true.
|
|
12147
12616
|
*/
|
|
12148
|
-
function ownProduct(
|
|
12149
|
-
if (own !== void 0 && own.packageName !==
|
|
12617
|
+
function ownProduct(server, own) {
|
|
12618
|
+
if (own !== void 0 && own.packageName !== serverFacts(server).packageName) throw new CommsError("UNEXPECTED", `${own.packageName} is not the ${serverLabel(server)} server's package, so it cannot register that server`);
|
|
12150
12619
|
return own;
|
|
12151
12620
|
}
|
|
12152
12621
|
/**
|
|
@@ -12156,11 +12625,11 @@ function ownProduct(channel, own) {
|
|
|
12156
12625
|
* register a server reaching every workspace for a caller who believed they had pinned it.
|
|
12157
12626
|
*/
|
|
12158
12627
|
function checkRequest(request) {
|
|
12159
|
-
if (!
|
|
12628
|
+
if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
12160
12629
|
if (!CLIENTS.includes(request.client)) throw new CommsError("USAGE", `"${String(request.client)}" is not a client this can register with`, { hint: `One of: ${CLIENTS.join(", ")}.` });
|
|
12161
12630
|
if (request.launcher !== void 0 && !LAUNCHERS.includes(request.launcher)) throw new CommsError("USAGE", `"${String(request.launcher)}" is not a launcher`, { hint: `One of: ${LAUNCHERS.join(", ")}.` });
|
|
12162
12631
|
if (request.name !== void 0) checkServerName(request.name);
|
|
12163
|
-
const label =
|
|
12632
|
+
const label = serverLabel(request.channel);
|
|
12164
12633
|
const refuse = (option) => {
|
|
12165
12634
|
const owner = narrowingOwner(option);
|
|
12166
12635
|
throw new CommsError("USAGE", owner === void 0 ? `\`${option}\` is not an option of the ${label} server` : `\`${option}\` is an option of the ${owner.label} server; the ${label} server has no such option`);
|
|
@@ -12196,7 +12665,7 @@ function checkPin(config, request) {
|
|
|
12196
12665
|
return;
|
|
12197
12666
|
}
|
|
12198
12667
|
const { account } = resolveName(config, "account", pin);
|
|
12199
|
-
if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${
|
|
12668
|
+
if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${serverLabel(request.channel)} ${accountNoun(request.channel)}`);
|
|
12200
12669
|
}
|
|
12201
12670
|
function installOptions(request) {
|
|
12202
12671
|
const own = pinOption(request.channel);
|
|
@@ -12215,7 +12684,7 @@ function installOptions(request) {
|
|
|
12215
12684
|
};
|
|
12216
12685
|
}
|
|
12217
12686
|
/**
|
|
12218
|
-
* Registering one
|
|
12687
|
+
* Registering one declared server with one client, as a change.
|
|
12219
12688
|
*
|
|
12220
12689
|
* The effects are what a person is agreeing to, one sentence each, and everything that decides what the server may
|
|
12221
12690
|
* reach is in them — which server, which client, under which name, pinned to what, replacing what, started how. The
|
|
@@ -12230,21 +12699,22 @@ function installOptions(request) {
|
|
|
12230
12699
|
* resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
|
|
12231
12700
|
* from another build of the checkout — is another registration, refused as one.
|
|
12232
12701
|
*/
|
|
12233
|
-
function serverInstallChange(core, env, request, own) {
|
|
12702
|
+
function serverInstallChange(core, env, request, own, deps = {}) {
|
|
12234
12703
|
checkRequest(request);
|
|
12235
12704
|
ownProduct(request.channel, own);
|
|
12236
12705
|
const context = {
|
|
12237
12706
|
env,
|
|
12238
|
-
core
|
|
12707
|
+
core,
|
|
12708
|
+
...deps.platform === void 0 ? {} : { platform: deps.platform }
|
|
12239
12709
|
};
|
|
12240
|
-
const facts =
|
|
12241
|
-
const label =
|
|
12710
|
+
const facts = serverFacts(request.channel);
|
|
12711
|
+
const label = serverLabel(request.channel);
|
|
12242
12712
|
const noun = accountNoun(request.channel);
|
|
12243
12713
|
const pinName = pinOption(request.channel);
|
|
12244
12714
|
const switchable = hasNarrowing(request.channel, "readOnly");
|
|
12245
12715
|
const name = request.name ?? facts.defaultServerName;
|
|
12246
12716
|
const launcher = request.launcher ?? "managed";
|
|
12247
|
-
const productNow = async () => own ?? await
|
|
12717
|
+
const productNow = async () => own ?? await serverProduct(request.channel, request.launcher);
|
|
12248
12718
|
let planned;
|
|
12249
12719
|
return {
|
|
12250
12720
|
plan: async (config) => {
|
|
@@ -12275,6 +12745,14 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12275
12745
|
else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
|
|
12276
12746
|
}
|
|
12277
12747
|
if (request.print !== true && launcher === "managed" && await reusableRuntime(core.paths.dataDir, facts.packageName, version) === null) effects.push(`installs ${facts.packageName}@${version} from npm into ${managedRuntimeDir(core.paths.dataDir, facts.packageName, version)}`);
|
|
12748
|
+
if (request.print !== true && launcher === "managed" && managedShimsSupported(context)) {
|
|
12749
|
+
const path = managedShimPath({
|
|
12750
|
+
env,
|
|
12751
|
+
dataDir: core.paths.dataDir,
|
|
12752
|
+
platform: context.platform
|
|
12753
|
+
}, facts.binary);
|
|
12754
|
+
effects.push(`writes or repoints the owned CLI shim at ${path}; a foreign file there is left unchanged`);
|
|
12755
|
+
}
|
|
12278
12756
|
return {
|
|
12279
12757
|
before: config,
|
|
12280
12758
|
after: config,
|
|
@@ -12284,9 +12762,10 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12284
12762
|
},
|
|
12285
12763
|
apply: async () => {
|
|
12286
12764
|
if (planned === void 0) throw new CommsError("UNEXPECTED", "the registration was applied before it was planned");
|
|
12287
|
-
const
|
|
12765
|
+
const installed = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
|
|
12288
12766
|
throw await remedyLocated(error, core);
|
|
12289
12767
|
});
|
|
12768
|
+
const result = request.print === true ? installed : await withManagedShim(context, planned.product, installed);
|
|
12290
12769
|
return {
|
|
12291
12770
|
...result,
|
|
12292
12771
|
restart: result.applied ? `Restart ${request.client} to load "${result.name}": no MCP client loads a new server into a session that is already running.` : null
|
|
@@ -12295,6 +12774,68 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12295
12774
|
};
|
|
12296
12775
|
}
|
|
12297
12776
|
/**
|
|
12777
|
+
* A managed install's CLI shim (CUE-406), once the registration's outcome is known: the command the locator finds in
|
|
12778
|
+
* the runtime the entry starts, written where nothing is or over this installation's own unchanged shim, and never
|
|
12779
|
+
* over anything else. A shim that cannot be written leaves the registration as it went, and says how to repair both.
|
|
12780
|
+
* The installer may not import the locator, so the operation writes it — for `mcp install` and `update` alike.
|
|
12781
|
+
*/
|
|
12782
|
+
async function withManagedShim(context, product, installed) {
|
|
12783
|
+
if (installed.launcher !== "managed") return installed;
|
|
12784
|
+
const cliEntry = installed.entry.args[0];
|
|
12785
|
+
if (cliEntry === void 0) throw new CommsError("UNEXPECTED", "the managed entry has no CLI path");
|
|
12786
|
+
const shimContext = {
|
|
12787
|
+
env: context.env,
|
|
12788
|
+
dataDir: context.core.paths.dataDir,
|
|
12789
|
+
platform: context.platform
|
|
12790
|
+
};
|
|
12791
|
+
if (!managedShimsSupported(shimContext)) return {
|
|
12792
|
+
...installed,
|
|
12793
|
+
shimNotice: managedShimUnsupportedNotice(shimContext)
|
|
12794
|
+
};
|
|
12795
|
+
const warnings = [...installed.warnings];
|
|
12796
|
+
let shim;
|
|
12797
|
+
let shimFailure;
|
|
12798
|
+
try {
|
|
12799
|
+
const located = locateCliCommand({
|
|
12800
|
+
caller: {
|
|
12801
|
+
url: pathToFileURL(cliEntry).href,
|
|
12802
|
+
packageName: product.packageName
|
|
12803
|
+
},
|
|
12804
|
+
target: {
|
|
12805
|
+
packageName: product.packageName,
|
|
12806
|
+
manifest: {
|
|
12807
|
+
label: product.defaultServerName,
|
|
12808
|
+
binary: product.binary
|
|
12809
|
+
}
|
|
12810
|
+
},
|
|
12811
|
+
words: [],
|
|
12812
|
+
uses: [],
|
|
12813
|
+
paths: {
|
|
12814
|
+
...context.core.paths,
|
|
12815
|
+
downloadsDir: context.core.paths.dataDir
|
|
12816
|
+
},
|
|
12817
|
+
platform: context.platform
|
|
12818
|
+
});
|
|
12819
|
+
if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
|
|
12820
|
+
shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
|
|
12821
|
+
binary: product.binary,
|
|
12822
|
+
command: located.command.words,
|
|
12823
|
+
entry: located.command.entry
|
|
12824
|
+
});
|
|
12825
|
+
} catch (error) {
|
|
12826
|
+
shimFailure = error instanceof Error ? error.message : String(error);
|
|
12827
|
+
const path = join(homeDirectory(context.env), ".local", "bin", product.binary);
|
|
12828
|
+
warnings.push(`The MCP registration ${installed.applied ? "succeeded" : "was not changed"}, but the CLI shim at ${path} could not be written (${shimFailure}). Fix that shim directory, then run this managed install again with \`--force\`; the owned registration is replaced without widening it and the shim is repaired.`);
|
|
12829
|
+
}
|
|
12830
|
+
if (shim?.status === "foreign") warnings.push(`${shim.path} already exists but was not written by this installation, so it was left unchanged; move it aside and run this managed install again with \`--force\` to create the CLI shim at that path.`);
|
|
12831
|
+
return {
|
|
12832
|
+
...installed,
|
|
12833
|
+
warnings,
|
|
12834
|
+
...shim === void 0 ? {} : { shim },
|
|
12835
|
+
...shimFailure === void 0 ? {} : { shimFailure }
|
|
12836
|
+
};
|
|
12837
|
+
}
|
|
12838
|
+
/**
|
|
12298
12839
|
* A refusal of `mcp install` with its remedy — another install of the same product — said with the command located:
|
|
12299
12840
|
* the printing package's own, core's, or another product's found among this machine's registrations (CUE-403). The
|
|
12300
12841
|
* installer says only the words; it may not import the locator.
|
|
@@ -12302,17 +12843,24 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12302
12843
|
async function remedyLocated(error, core) {
|
|
12303
12844
|
const remedy = installRemedyOf(error);
|
|
12304
12845
|
if (remedy === void 0 || !(error instanceof CommsError)) return error;
|
|
12305
|
-
const
|
|
12306
|
-
if (
|
|
12846
|
+
const server = SERVERS.find((each) => serverFacts(each).binary === remedy.binary);
|
|
12847
|
+
if (server === void 0) return error;
|
|
12307
12848
|
const maker = await requireHandoffs(core).registered();
|
|
12849
|
+
const handoff = isChannel(server) ? maker.of(server, remedy.words) : maker.core([
|
|
12850
|
+
"mcp",
|
|
12851
|
+
"install",
|
|
12852
|
+
"--server",
|
|
12853
|
+
server,
|
|
12854
|
+
...remedy.words.slice(2)
|
|
12855
|
+
]);
|
|
12308
12856
|
return new CommsError(error.code, error.message, {
|
|
12309
|
-
hint: handoffSentence(
|
|
12857
|
+
hint: handoffSentence(handoff, remedy.say),
|
|
12310
12858
|
...error.details === void 0 ? {} : { details: error.details },
|
|
12311
12859
|
...error.cause === void 0 ? {} : { cause: error.cause }
|
|
12312
12860
|
});
|
|
12313
12861
|
}
|
|
12314
12862
|
/**
|
|
12315
|
-
* Removing a
|
|
12863
|
+
* Removing a declared server's unused managed runtimes, as a change.
|
|
12316
12864
|
*
|
|
12317
12865
|
* A dry run is free: it removes nothing. Otherwise the plan is that same dry run, and each runtime it would remove is
|
|
12318
12866
|
* one effect — the removal cannot be taken back — so the approval is bound to exactly that list; and the removal is
|
|
@@ -12322,20 +12870,20 @@ async function remedyLocated(error, core) {
|
|
|
12322
12870
|
* release of the command that was run; checked to be that channel's, as for an install.
|
|
12323
12871
|
*/
|
|
12324
12872
|
function serverPruneChange(core, env, request, own) {
|
|
12325
|
-
if (!
|
|
12873
|
+
if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
12326
12874
|
const context = {
|
|
12327
12875
|
env,
|
|
12328
12876
|
core
|
|
12329
12877
|
};
|
|
12330
12878
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
12331
|
-
packageName:
|
|
12332
|
-
version: "0.
|
|
12879
|
+
packageName: serverFacts(request.channel).packageName,
|
|
12880
|
+
version: "0.18.1"
|
|
12333
12881
|
};
|
|
12334
12882
|
const product = {
|
|
12335
12883
|
packageName,
|
|
12336
12884
|
version
|
|
12337
12885
|
};
|
|
12338
|
-
const label =
|
|
12886
|
+
const label = serverLabel(request.channel);
|
|
12339
12887
|
const base = {
|
|
12340
12888
|
includePrinted: request.includePrinted === true,
|
|
12341
12889
|
...request.processes ? { processes: request.processes } : {}
|
|
@@ -12356,7 +12904,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
12356
12904
|
return {
|
|
12357
12905
|
before: config,
|
|
12358
12906
|
after: config,
|
|
12359
|
-
effects: preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`),
|
|
12907
|
+
effects: [...preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`), ...preview.shims.removed.map((path) => `deletes the owned ${label} CLI shim at ${path}`)],
|
|
12360
12908
|
summary: `Remove ${approved.length} unused ${label} runtime${approved.length === 1 ? "" : "s"}`
|
|
12361
12909
|
};
|
|
12362
12910
|
},
|
|
@@ -12635,21 +13183,22 @@ function updateStopMessage(pending, where, commands) {
|
|
|
12635
13183
|
* the yes is a different change, and the claim is refused. Nothing behind, nothing is prepared.
|
|
12636
13184
|
*/
|
|
12637
13185
|
/**
|
|
12638
|
-
* The packages this suite publishes, whose latest releases an update reads: every
|
|
12639
|
-
* every package a
|
|
13186
|
+
* The packages this suite publishes, whose latest releases an update reads: every declared server owner's, from the
|
|
13187
|
+
* channel and service declarations, and every package a server is run through by `npx` when that is another one —
|
|
13188
|
+
* Gmail's `gmail-mcp`.
|
|
12640
13189
|
*/
|
|
12641
|
-
const
|
|
12642
|
-
const SERVER_PACKAGES = Object.freeze(
|
|
12643
|
-
const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...
|
|
12644
|
-
function narrowingArgs(
|
|
12645
|
-
return
|
|
13190
|
+
const OWNER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).packageName));
|
|
13191
|
+
const SERVER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).npxPackage).filter((name) => !OWNER_PACKAGES.includes(name)));
|
|
13192
|
+
const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...OWNER_PACKAGES, ...SERVER_PACKAGES])].sort());
|
|
13193
|
+
function narrowingArgs(server, narrowing) {
|
|
13194
|
+
return serverFacts(server).serverArgs({
|
|
12646
13195
|
client: "json",
|
|
12647
13196
|
...narrowing
|
|
12648
13197
|
});
|
|
12649
13198
|
}
|
|
12650
|
-
/** Every registration of every
|
|
13199
|
+
/** Every registration of every declared server, as the core's `channels` finds them — nothing an `env` holds. */
|
|
12651
13200
|
function registrations(servers) {
|
|
12652
|
-
return
|
|
13201
|
+
return SERVERS.flatMap((channel) => servers.filter((server) => isProductServer(server, serverFacts(channel))).map((server) => ({
|
|
12653
13202
|
channel,
|
|
12654
13203
|
server
|
|
12655
13204
|
})));
|
|
@@ -12687,7 +13236,7 @@ async function latestReleases(names, latestVersion) {
|
|
|
12687
13236
|
* have no line to paste: the reasons below give it with `handoffSentence`, which shows it as words then.
|
|
12688
13237
|
*/
|
|
12689
13238
|
function installHandoff(item, handoffs) {
|
|
12690
|
-
const facts =
|
|
13239
|
+
const facts = serverFacts(item.channel);
|
|
12691
13240
|
const words = [
|
|
12692
13241
|
"mcp",
|
|
12693
13242
|
"install",
|
|
@@ -12696,7 +13245,13 @@ function installHandoff(item, handoffs) {
|
|
|
12696
13245
|
];
|
|
12697
13246
|
if (item.name !== facts.defaultServerName) words.push("--name", item.name);
|
|
12698
13247
|
words.push(...item.narrowing, "--force");
|
|
12699
|
-
return handoffs.of(item.channel, words)
|
|
13248
|
+
return isChannel(item.channel) ? handoffs.of(item.channel, words) : handoffs.core([
|
|
13249
|
+
"mcp",
|
|
13250
|
+
"install",
|
|
13251
|
+
"--server",
|
|
13252
|
+
item.channel,
|
|
13253
|
+
...words.slice(2)
|
|
13254
|
+
]);
|
|
12700
13255
|
}
|
|
12701
13256
|
/**
|
|
12702
13257
|
* Why a registration that is behind cannot be registered again from here, or undefined when it can.
|
|
@@ -12736,8 +13291,8 @@ function newest(versions) {
|
|
|
12736
13291
|
*/
|
|
12737
13292
|
async function runtimesNeeded(dataDir, latest, managed) {
|
|
12738
13293
|
const needed = [];
|
|
12739
|
-
for (const channel of
|
|
12740
|
-
const packageName =
|
|
13294
|
+
for (const channel of SERVERS) {
|
|
13295
|
+
const packageName = serverFacts(channel).packageName;
|
|
12741
13296
|
const version = latest[packageName];
|
|
12742
13297
|
if (version === void 0) continue;
|
|
12743
13298
|
if (!managed.some((item) => item.channel === channel && item.launcher === "managed")) continue;
|
|
@@ -12764,7 +13319,7 @@ async function inspect(core, env, deps) {
|
|
|
12764
13319
|
const handoffs = requireHandoffs(core, deps.platform).withRegistrations(scan.servers);
|
|
12765
13320
|
const unreadable = [...scan.unreadable];
|
|
12766
13321
|
const found = registrations(scan.servers).map(({ channel, server }) => {
|
|
12767
|
-
const facts =
|
|
13322
|
+
const facts = serverFacts(channel);
|
|
12768
13323
|
return {
|
|
12769
13324
|
channel,
|
|
12770
13325
|
server,
|
|
@@ -12784,9 +13339,9 @@ async function inspect(core, env, deps) {
|
|
|
12784
13339
|
reason: error instanceof Error ? error.message : String(error)
|
|
12785
13340
|
});
|
|
12786
13341
|
}
|
|
12787
|
-
const wanted = /* @__PURE__ */ new Set([
|
|
13342
|
+
const wanted = /* @__PURE__ */ new Set([serverFacts("core").packageName]);
|
|
12788
13343
|
for (const entry of found) {
|
|
12789
|
-
wanted.add(
|
|
13344
|
+
wanted.add(serverFacts(entry.channel).packageName);
|
|
12790
13345
|
wanted.add(entry.package);
|
|
12791
13346
|
}
|
|
12792
13347
|
for (const name of Object.keys(installed)) wanted.add(name);
|
|
@@ -12809,7 +13364,7 @@ async function inspect(core, env, deps) {
|
|
|
12809
13364
|
name: entry.server.name,
|
|
12810
13365
|
scope: entry.server.scope ?? "user",
|
|
12811
13366
|
path: entry.server.path,
|
|
12812
|
-
launcher: launcherOf(entry.server,
|
|
13367
|
+
launcher: launcherOf(entry.server, serverFacts(entry.channel)),
|
|
12813
13368
|
version: entry.version,
|
|
12814
13369
|
latest: latest[entry.package] ?? "",
|
|
12815
13370
|
narrowing: narrowingArgs(entry.channel, entry.narrowing)
|
|
@@ -12840,8 +13395,8 @@ async function inspect(core, env, deps) {
|
|
|
12840
13395
|
}
|
|
12841
13396
|
const needed = await runtimesNeeded(core.paths.dataDir, latest, candidates.map((candidate) => candidate.item));
|
|
12842
13397
|
behind.push(...needed);
|
|
12843
|
-
for (const channel of
|
|
12844
|
-
const packageName =
|
|
13398
|
+
for (const channel of SERVERS) {
|
|
13399
|
+
const packageName = serverFacts(channel).packageName;
|
|
12845
13400
|
const version = latest[packageName];
|
|
12846
13401
|
if (version === void 0 || await reusableRuntime(core.paths.dataDir, packageName, version) === null) continue;
|
|
12847
13402
|
upToDate.push({
|
|
@@ -12899,7 +13454,7 @@ async function updateCheck(core, env, deps = {}, claim) {
|
|
|
12899
13454
|
await recordFound(core, env, report, deps, null, claim);
|
|
12900
13455
|
return report;
|
|
12901
13456
|
}
|
|
12902
|
-
const CORE_PACKAGE$1 =
|
|
13457
|
+
const CORE_PACKAGE$1 = serverFacts("core").packageName;
|
|
12903
13458
|
/**
|
|
12904
13459
|
* One registration: its channel, its client, its scope, the file it is in and its name. Two copies of it in one file —
|
|
12905
13460
|
* `servers` and an old `mcpServers` — are one registration, and an update registers them again as one.
|
|
@@ -12978,7 +13533,7 @@ function updateCheckFindings(report, latest, after = null) {
|
|
|
12978
13533
|
});
|
|
12979
13534
|
const globals = items.filter((item) => item.kind === "global");
|
|
12980
13535
|
const global = CHANNELS.filter((channel) => {
|
|
12981
|
-
const name =
|
|
13536
|
+
const name = serverFacts(channel).packageName;
|
|
12982
13537
|
return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.version));
|
|
12983
13538
|
});
|
|
12984
13539
|
const stillBehind = after === null ? report.behind.length > 0 : !(after.ok && after.manual.length === 0 && after.status !== "manual");
|
|
@@ -13009,13 +13564,11 @@ function reachOf(channel, narrowing) {
|
|
|
13009
13564
|
*/
|
|
13010
13565
|
function registrationEffects(step) {
|
|
13011
13566
|
const { item } = step;
|
|
13012
|
-
const reach = reachOf(item.channel,
|
|
13013
|
-
const
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
`${item.client} will fetch ${item.package}@${item.latest} from npm each time it starts "${item.name}"`
|
|
13018
|
-
] : [entry, step.destination];
|
|
13567
|
+
const reach = reachOf(item.channel, serverFacts(item.channel).narrowingOf(item.narrowing));
|
|
13568
|
+
const effects = [`registers the ${serverLabel(item.channel)} MCP server with ${item.client} as "${item.name}" again (${item.scope} scope, ${item.launcher} launcher), at ${item.latest} in place of ${item.version}${reach ? ` — ${reach}, as now` : ""}`, step.destination];
|
|
13569
|
+
if (step.shim) effects.push(`writes or repoints the owned CLI shim at ${step.shim}; a foreign file there is left unchanged`);
|
|
13570
|
+
if (item.launcher === "npx") effects.push(`${item.client} will fetch ${item.package}@${item.latest} from npm each time it starts "${item.name}"`);
|
|
13571
|
+
return effects;
|
|
13019
13572
|
}
|
|
13020
13573
|
/** Every step as the sentences a person reads, in the order the steps are taken. */
|
|
13021
13574
|
function effectsOf(planned) {
|
|
@@ -13051,7 +13604,7 @@ async function planRegistrations(context, candidates, request, handoffs) {
|
|
|
13051
13604
|
if (seen.has(key)) continue;
|
|
13052
13605
|
seen.add(key);
|
|
13053
13606
|
const launcher = item.launcher;
|
|
13054
|
-
const product = await
|
|
13607
|
+
const product = await serverProduct(item.channel, launcher, item.latest);
|
|
13055
13608
|
const options = {
|
|
13056
13609
|
client: item.client,
|
|
13057
13610
|
name: item.name,
|
|
@@ -13077,7 +13630,12 @@ async function planRegistrations(context, candidates, request, handoffs) {
|
|
|
13077
13630
|
product,
|
|
13078
13631
|
options,
|
|
13079
13632
|
install: plannedInstall(preflight),
|
|
13080
|
-
destination: entryDestination(item.client, preflight.target, preflight.command, context.env)
|
|
13633
|
+
destination: entryDestination(item.client, preflight.target, preflight.command, context.env),
|
|
13634
|
+
...launcher === "managed" && managedShimsSupported(context) ? { shim: managedShimPath({
|
|
13635
|
+
env: context.env,
|
|
13636
|
+
dataDir: context.core.paths.dataDir,
|
|
13637
|
+
platform: context.platform
|
|
13638
|
+
}, product.binary) } : {}
|
|
13081
13639
|
});
|
|
13082
13640
|
}
|
|
13083
13641
|
return {
|
|
@@ -13199,18 +13757,23 @@ function whatBecameOf(stored) {
|
|
|
13199
13757
|
revoked: "it had been revoked"
|
|
13200
13758
|
}[record.state] ?? `it is ${JSON.stringify(record.state)}, a state this version does not know`;
|
|
13201
13759
|
}
|
|
13202
|
-
/** The product a runtime of `packageName` is installed as: the
|
|
13760
|
+
/** The product a runtime of `packageName` is installed as: the declared server whose package it is. */
|
|
13203
13761
|
function runtimeProduct(packageName, version) {
|
|
13204
|
-
const channel =
|
|
13205
|
-
if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a
|
|
13206
|
-
return
|
|
13762
|
+
const channel = SERVERS.find((each) => serverFacts(each).packageName === packageName);
|
|
13763
|
+
if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a declared server package`);
|
|
13764
|
+
return serverProduct(channel, "managed", version);
|
|
13207
13765
|
}
|
|
13208
13766
|
/**
|
|
13209
|
-
* Every
|
|
13210
|
-
*
|
|
13767
|
+
* Every declared server's `mcp prune`, as a person runs it at a terminal: each channel command is located among this
|
|
13768
|
+
* machine's registrations, and each service uses core — never a bare name (CUE-403).
|
|
13211
13769
|
*/
|
|
13212
13770
|
function pruneCommands(handoffs) {
|
|
13213
|
-
return handoffChoices(
|
|
13771
|
+
return handoffChoices(SERVERS.map((channel) => isChannel(channel) ? handoffs.of(channel, ["mcp", "prune"]) : handoffs.core([
|
|
13772
|
+
"mcp",
|
|
13773
|
+
"prune",
|
|
13774
|
+
"--server",
|
|
13775
|
+
channel
|
|
13776
|
+
])), "none of their commands is locatable here:", { conjunction: "and" });
|
|
13214
13777
|
}
|
|
13215
13778
|
function message(error) {
|
|
13216
13779
|
return error instanceof CommsError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -13267,26 +13830,28 @@ async function applyUpdate(context, planned, deps) {
|
|
|
13267
13830
|
continue;
|
|
13268
13831
|
}
|
|
13269
13832
|
try {
|
|
13270
|
-
const pins =
|
|
13271
|
-
const
|
|
13833
|
+
const pins = serverFacts(item.channel).narrowingOf(item.narrowing);
|
|
13834
|
+
const installed = await mcpInstall(context, product, {
|
|
13272
13835
|
...options,
|
|
13273
13836
|
...pins
|
|
13274
13837
|
}, install);
|
|
13275
|
-
if (!
|
|
13838
|
+
if (!installed.applied) {
|
|
13276
13839
|
steps.push({
|
|
13277
13840
|
...base,
|
|
13278
13841
|
outcome: "failed",
|
|
13279
|
-
detail:
|
|
13842
|
+
detail: installed.notApplied ?? "nothing was registered"
|
|
13280
13843
|
});
|
|
13281
13844
|
continue;
|
|
13282
13845
|
}
|
|
13846
|
+
const result = await withManagedShim(context, product, installed);
|
|
13283
13847
|
steps.push({
|
|
13284
13848
|
...base,
|
|
13285
13849
|
outcome: "registered",
|
|
13286
13850
|
verification: result.verification,
|
|
13287
13851
|
...result.verifyDetail !== void 0 ? { detail: result.verifyDetail } : {},
|
|
13288
13852
|
...result.backupPath !== void 0 ? { backupPath: result.backupPath } : {},
|
|
13289
|
-
...result.warnings.length > 0 ? { warnings: result.warnings } : {}
|
|
13853
|
+
...result.warnings.length > 0 ? { warnings: result.warnings } : {},
|
|
13854
|
+
...result.shim !== void 0 ? { shim: result.shim } : {}
|
|
13290
13855
|
});
|
|
13291
13856
|
} catch (error) {
|
|
13292
13857
|
steps.push({
|
|
@@ -14046,7 +14611,7 @@ function terminalUpdateHooks(core, env, options) {
|
|
|
14046
14611
|
};
|
|
14047
14612
|
}
|
|
14048
14613
|
//#endregion
|
|
14049
|
-
//#region ../core/dist/tool-arguments-
|
|
14614
|
+
//#region ../core/dist/tool-arguments-DLa_xsSp.mjs
|
|
14050
14615
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
14051
14616
|
const TARGET = "draft-2020-12";
|
|
14052
14617
|
/**
|
|
@@ -14351,7 +14916,7 @@ function spoken(words, joiner) {
|
|
|
14351
14916
|
return `${words.slice(0, -1).join(", ")} ${joiner} ${words.at(-1)}`;
|
|
14352
14917
|
}
|
|
14353
14918
|
//#endregion
|
|
14354
|
-
//#region ../../node_modules/.pnpm/postal-mime@
|
|
14919
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/decode-strings.js
|
|
14355
14920
|
const textEncoder = new TextEncoder();
|
|
14356
14921
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
14357
14922
|
const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
|
|
@@ -14406,26 +14971,143 @@ const codePagePattern = /^(?:cp|windows|ms|ibm)(\d+)$/;
|
|
|
14406
14971
|
function normalizeCharset(charset) {
|
|
14407
14972
|
return charset.replace(/^(?:x-ms-|x-|cs)/, "").replace(/[\s._-]+/g, "");
|
|
14408
14973
|
}
|
|
14974
|
+
const ENCODING_LABELS = new Set([
|
|
14975
|
+
"unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8",
|
|
14976
|
+
"866 cp866 csibm866 ibm866",
|
|
14977
|
+
"csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2",
|
|
14978
|
+
"csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3",
|
|
14979
|
+
"csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4",
|
|
14980
|
+
"csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988",
|
|
14981
|
+
"arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e",
|
|
14982
|
+
"iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987",
|
|
14983
|
+
"csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7",
|
|
14984
|
+
"iso_8859-7:1987 sun_eu_greek",
|
|
14985
|
+
"csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8",
|
|
14986
|
+
"iso_8859-8:1988 visual",
|
|
14987
|
+
"csiso88598i iso-8859-8-i logical",
|
|
14988
|
+
"csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6",
|
|
14989
|
+
"iso-8859-13 iso8859-13 iso885913",
|
|
14990
|
+
"iso-8859-14 iso8859-14 iso885914",
|
|
14991
|
+
"csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9",
|
|
14992
|
+
"iso-8859-16",
|
|
14993
|
+
"cskoi8r koi koi8 koi8-r koi8_r",
|
|
14994
|
+
"koi8-ru koi8-u",
|
|
14995
|
+
"csmacintosh mac macintosh x-mac-roman",
|
|
14996
|
+
"dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874",
|
|
14997
|
+
"cp1250 windows-1250 x-cp1250",
|
|
14998
|
+
"cp1251 windows-1251 x-cp1251",
|
|
14999
|
+
"ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591",
|
|
15000
|
+
"iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252",
|
|
15001
|
+
"cp1253 windows-1253 x-cp1253",
|
|
15002
|
+
"cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5",
|
|
15003
|
+
"windows-1254 x-cp1254",
|
|
15004
|
+
"cp1255 windows-1255 x-cp1255",
|
|
15005
|
+
"cp1256 windows-1256 x-cp1256",
|
|
15006
|
+
"cp1257 windows-1257 x-cp1257",
|
|
15007
|
+
"cp1258 windows-1258 x-cp1258",
|
|
15008
|
+
"x-mac-cyrillic x-mac-ukrainian",
|
|
15009
|
+
"chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk",
|
|
15010
|
+
"gb18030",
|
|
15011
|
+
"big5 big5-hkscs cn-big5 csbig5 x-x-big5",
|
|
15012
|
+
"cseucpkdfmtjapanese euc-jp x-euc-jp",
|
|
15013
|
+
"csiso2022jp iso-2022-jp",
|
|
15014
|
+
"csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis",
|
|
15015
|
+
"cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
|
|
15016
|
+
"windows-949",
|
|
15017
|
+
"csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
|
|
15018
|
+
"unicodefffe utf-16be",
|
|
15019
|
+
"csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
|
|
15020
|
+
"x-user-defined"
|
|
15021
|
+
].join(" ").split(" "));
|
|
15022
|
+
const decoders = /* @__PURE__ */ new Map();
|
|
14409
15023
|
function tryDecoder(charset) {
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
15024
|
+
if (!ENCODING_LABELS.has(charset)) return null;
|
|
15025
|
+
let decoder = decoders.get(charset);
|
|
15026
|
+
if (decoder === void 0) {
|
|
15027
|
+
decoder = null;
|
|
15028
|
+
try {
|
|
15029
|
+
decoder = new TextDecoder(charset);
|
|
15030
|
+
} catch {}
|
|
15031
|
+
decoders.set(charset, decoder);
|
|
14414
15032
|
}
|
|
15033
|
+
return decoder;
|
|
14415
15034
|
}
|
|
14416
|
-
|
|
15035
|
+
const utf8Decoder = new TextDecoder();
|
|
15036
|
+
/**
|
|
15037
|
+
* Finds the decoder for a charset label, or null when the runtime has no encoding for it
|
|
15038
|
+
*/
|
|
15039
|
+
function findDecoder(charset) {
|
|
14417
15040
|
charset = (charset || "utf8").trim().toLowerCase();
|
|
14418
15041
|
const decoder = tryDecoder(charset);
|
|
14419
15042
|
if (decoder) return decoder;
|
|
14420
15043
|
const normalized = normalizeCharset(charset);
|
|
14421
15044
|
const codePage = normalized.match(codePagePattern);
|
|
14422
|
-
return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized)
|
|
15045
|
+
return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized);
|
|
15046
|
+
}
|
|
15047
|
+
function getDecoder(charset) {
|
|
15048
|
+
return findDecoder(charset) || tryDecoder("windows-1252") || utf8Decoder;
|
|
15049
|
+
}
|
|
15050
|
+
/**
|
|
15051
|
+
* Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
|
|
15052
|
+
* the exception one throws costs more than the decode itself and a message decides how
|
|
15053
|
+
* many lines throw it.
|
|
15054
|
+
*/
|
|
15055
|
+
function isUtf8(bytes) {
|
|
15056
|
+
for (let i = 0; i < bytes.length;) {
|
|
15057
|
+
const lead = bytes[i];
|
|
15058
|
+
if (lead < 128) {
|
|
15059
|
+
i++;
|
|
15060
|
+
continue;
|
|
15061
|
+
}
|
|
15062
|
+
let count;
|
|
15063
|
+
if (lead >= 194 && lead <= 223) count = 1;
|
|
15064
|
+
else if (lead >= 224 && lead <= 239) count = 2;
|
|
15065
|
+
else if (lead >= 240 && lead <= 244) count = 3;
|
|
15066
|
+
else return false;
|
|
15067
|
+
if (i + count >= bytes.length) return false;
|
|
15068
|
+
const second = bytes[i + 1];
|
|
15069
|
+
if (second < (lead === 224 ? 160 : lead === 240 ? 144 : 128) || second > (lead === 237 ? 159 : lead === 244 ? 143 : 191)) return false;
|
|
15070
|
+
for (let k = 2; k <= count; k++) if ((bytes[i + k] & 192) !== 128) return false;
|
|
15071
|
+
i += count + 1;
|
|
15072
|
+
}
|
|
15073
|
+
return true;
|
|
15074
|
+
}
|
|
15075
|
+
/**
|
|
15076
|
+
* Decodes bytes whose encoding is not known: as UTF-8 when they are well formed UTF-8, as
|
|
15077
|
+
* windows-1252 otherwise.
|
|
15078
|
+
*
|
|
15079
|
+
* A message that declares no charset, or one the runtime has never heard of, usually
|
|
15080
|
+
* predates RFC 6532 or comes from a generator that never heard of it either, and its 8-bit
|
|
15081
|
+
* bytes are a legacy single byte encoding far more often than broken UTF-8. windows-1252
|
|
15082
|
+
* is the fallback the WHATWG Encoding Standard prescribes and what a mail client's default
|
|
15083
|
+
* charset amounts to, and it reads such text rather than filling it with replacement
|
|
15084
|
+
* characters.
|
|
15085
|
+
*
|
|
15086
|
+
* @param bytes Bytes to decode
|
|
15087
|
+
* @param utf8 The UTF-8 decoder to use, header lines bring one that keeps a BOM
|
|
15088
|
+
* @return Decoded text
|
|
15089
|
+
*/
|
|
15090
|
+
function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
|
|
15091
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
15092
|
+
return isUtf8(view) ? utf8.decode(view) : getDecoder("windows-1252").decode(view);
|
|
15093
|
+
}
|
|
15094
|
+
/**
|
|
15095
|
+
* Decodes bytes in a named charset. A label the runtime does not know is no charset at
|
|
15096
|
+
* all, see decodeUtf8OrWindows1252.
|
|
15097
|
+
*
|
|
15098
|
+
* @param bytes Bytes to decode
|
|
15099
|
+
* @param charset Charset label
|
|
15100
|
+
* @return Decoded text
|
|
15101
|
+
*/
|
|
15102
|
+
function decodeCharset(bytes, charset) {
|
|
15103
|
+
const decoder = findDecoder(charset);
|
|
15104
|
+
return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
|
|
14423
15105
|
}
|
|
14424
15106
|
/**
|
|
14425
15107
|
* Numeric value of an ASCII hex digit
|
|
14426
15108
|
*
|
|
14427
|
-
* @param
|
|
14428
|
-
* @return
|
|
15109
|
+
* @param c Byte to read
|
|
15110
|
+
* @return Value 0-15, or -1 if the byte is not a hex digit
|
|
14429
15111
|
*/
|
|
14430
15112
|
function hexNibble(c) {
|
|
14431
15113
|
if (c >= 48 && c <= 57) return c - 48;
|
|
@@ -14434,14 +15116,35 @@ function hexNibble(c) {
|
|
|
14434
15116
|
return -1;
|
|
14435
15117
|
}
|
|
14436
15118
|
/**
|
|
15119
|
+
* Folds the line breaks of a header value into spaces.
|
|
15120
|
+
*
|
|
15121
|
+
* A header value holds no line breaks. The raw value is folded when the headers are
|
|
15122
|
+
* processed, see processHeaders in mime-node.ts, an RFC 2047 encoded word is folded as it
|
|
15123
|
+
* is decoded, and an RFC 2231 filename is folded where the attachment is built. Otherwise a
|
|
15124
|
+
* decoded name, subject or filename written into a header of an outgoing message by the
|
|
15125
|
+
* consumer ends the header early and starts another one with the rest of the value.
|
|
15126
|
+
*
|
|
15127
|
+
* The RFC 2231 percent decoder itself does not fold: it also produces the boundary, and a
|
|
15128
|
+
* boundary holding a line break can never match a line. That has to stay so, or a message
|
|
15129
|
+
* that has no parts for every other parser would have them here.
|
|
15130
|
+
*
|
|
15131
|
+
* @param str Header value
|
|
15132
|
+
* @return The value with every run of CR and LF replaced by a space
|
|
15133
|
+
*/
|
|
15134
|
+
function foldLineBreaks(str) {
|
|
15135
|
+
return str.replace(/[\r\n]+/g, " ");
|
|
15136
|
+
}
|
|
15137
|
+
/**
|
|
14437
15138
|
* Decode a complete mime word encoded string
|
|
14438
15139
|
*
|
|
14439
|
-
* @param
|
|
14440
|
-
* @
|
|
15140
|
+
* @param charset Character set of the encoded word
|
|
15141
|
+
* @param encoding `Q` or `B`
|
|
15142
|
+
* @param str Mime word encoded string
|
|
15143
|
+
* @return Decoded unicode string
|
|
14441
15144
|
*/
|
|
14442
15145
|
function decodeWord(charset, encoding, str) {
|
|
14443
15146
|
let splitPos = charset.indexOf("*");
|
|
14444
|
-
if (splitPos >= 0) charset = charset.
|
|
15147
|
+
if (splitPos >= 0) charset = charset.slice(0, splitPos);
|
|
14445
15148
|
encoding = encoding.toUpperCase();
|
|
14446
15149
|
let byteStr;
|
|
14447
15150
|
if (encoding === "Q") {
|
|
@@ -14466,7 +15169,7 @@ function decodeWord(charset, encoding, str) {
|
|
|
14466
15169
|
for (let i = 0, len = encodedBytes.length; i < len; i++) dataView.setUint8(i, encodedBytes[i]);
|
|
14467
15170
|
} else if (encoding === "B") byteStr = decodeBase64$1(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ""));
|
|
14468
15171
|
else byteStr = textEncoder.encode(str);
|
|
14469
|
-
return
|
|
15172
|
+
return foldLineBreaks(decodeCharset(byteStr, charset));
|
|
14470
15173
|
}
|
|
14471
15174
|
const ENCODED_WORD_PATTERN = "=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=";
|
|
14472
15175
|
const ENCODED_WORD_REGEX = new RegExp(ENCODED_WORD_PATTERN, "g");
|
|
@@ -14476,8 +15179,8 @@ const ENCODED_WORDS_ONLY_REGEX = new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`
|
|
|
14476
15179
|
* Checks whether a string is nothing but RFC 2047 encoded words. Kept next to the grammar
|
|
14477
15180
|
* it depends on, so the pattern has a single definition.
|
|
14478
15181
|
*
|
|
14479
|
-
* @param
|
|
14480
|
-
* @return
|
|
15182
|
+
* @param str String to check
|
|
15183
|
+
* @return true if the string holds encoded words and nothing else
|
|
14481
15184
|
*/
|
|
14482
15185
|
function isEncodedWordsOnly(str) {
|
|
14483
15186
|
return ENCODED_WORDS_ONLY_REGEX.test(str);
|
|
@@ -14489,8 +15192,8 @@ function isEncodedWordsOnly(str) {
|
|
|
14489
15192
|
* means the input can not contain the marker, which previously let a sender delete text
|
|
14490
15193
|
* from a subject or a display name by writing the marker into the header themselves.
|
|
14491
15194
|
*
|
|
14492
|
-
* @param
|
|
14493
|
-
* @return
|
|
15195
|
+
* @param str String to split
|
|
15196
|
+
* @return Array of `{text}` and `{charset, encoding, encodedText}` tokens
|
|
14494
15197
|
*/
|
|
14495
15198
|
function splitEncodedWords(str) {
|
|
14496
15199
|
const tokens = [];
|
|
@@ -14524,9 +15227,9 @@ function canJoinWords(left, right) {
|
|
|
14524
15227
|
/**
|
|
14525
15228
|
* Decodes a token list into a string, optionally merging adjacent encoded words.
|
|
14526
15229
|
*
|
|
14527
|
-
* @param
|
|
14528
|
-
* @param
|
|
14529
|
-
* @return
|
|
15230
|
+
* @param tokens Tokens from splitEncodedWords
|
|
15231
|
+
* @param joinWords Whether adjacent encoded words may be decoded as one unit
|
|
15232
|
+
* @return Decoded string
|
|
14530
15233
|
*/
|
|
14531
15234
|
function renderTokens(tokens, joinWords) {
|
|
14532
15235
|
let result = "";
|
|
@@ -14557,6 +15260,12 @@ function renderTokens(tokens, joinWords) {
|
|
|
14557
15260
|
if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
14558
15261
|
return result;
|
|
14559
15262
|
}
|
|
15263
|
+
/**
|
|
15264
|
+
* Decodes RFC 2047 encoded words in a string
|
|
15265
|
+
*
|
|
15266
|
+
* @param str String that may contain encoded words
|
|
15267
|
+
* @return Unicode string with every encoded word decoded
|
|
15268
|
+
*/
|
|
14560
15269
|
function decodeWords(str) {
|
|
14561
15270
|
const tokens = splitEncodedWords((str || "").toString());
|
|
14562
15271
|
const result = renderTokens(tokens, true);
|
|
@@ -14564,55 +15273,279 @@ function decodeWords(str) {
|
|
|
14564
15273
|
}
|
|
14565
15274
|
new TextDecoder("utf-8", { ignoreBOM: true });
|
|
14566
15275
|
//#endregion
|
|
14567
|
-
//#region ../../node_modules/.pnpm/postal-mime@
|
|
14568
|
-
const WORD_CHAR_REGEX = /\w/;
|
|
14569
|
-
const NON_SPACE_TOKEN_REGEX = /[^\s]+/g;
|
|
15276
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/address-parser.js
|
|
14570
15277
|
/**
|
|
14571
|
-
*
|
|
15278
|
+
* Any whitespace, the test for an angle-addr that needs recovering
|
|
15279
|
+
*/
|
|
15280
|
+
const HAS_WHITESPACE = /\s/;
|
|
15281
|
+
/**
|
|
15282
|
+
* An addr-spec that carries its whitespace legally, inside a quoted local part. The
|
|
15283
|
+
* optional tail is the malformed shape: a real mailbox with wreckage trailing it.
|
|
15284
|
+
*/
|
|
15285
|
+
const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
15286
|
+
/**
|
|
15287
|
+
* One run holding a single '@' and no whitespace, the shape an addr-spec has to have
|
|
15288
|
+
*/
|
|
15289
|
+
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
15290
|
+
/**
|
|
15291
|
+
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
15292
|
+
* further '@' that a domain should not have but malformed headers carry anyway
|
|
15293
|
+
*/
|
|
15294
|
+
const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
15295
|
+
/**
|
|
15296
|
+
* An addr-spec sitting inside free text, together with the whitespace around it. Sticky
|
|
15297
|
+
* on purpose: it is only run at the offset looseAddressStart picks, see there.
|
|
15298
|
+
*/
|
|
15299
|
+
const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
15300
|
+
/**
|
|
15301
|
+
* Restores the quoting of a local part that was read out of a quoted string.
|
|
15302
|
+
*
|
|
15303
|
+
* RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
|
|
15304
|
+
* on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
|
|
15305
|
+
* domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
|
|
15306
|
+
* same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
|
|
15307
|
+
* recipient separator once the consumer puts the address back into a header.
|
|
15308
|
+
*
|
|
15309
|
+
* @param address Address with an unquoted local part
|
|
15310
|
+
* @return Address with the local part as a quoted-string
|
|
15311
|
+
*/
|
|
15312
|
+
function quoteLocalPart(address) {
|
|
15313
|
+
const lastAt = address.lastIndexOf("@");
|
|
15314
|
+
if (lastAt < 0) return address;
|
|
15315
|
+
const user = address.substring(0, lastAt);
|
|
15316
|
+
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) return address;
|
|
15317
|
+
return "\"" + user.replace(/["\\]/g, "\\$&") + "\"@" + address.substring(lastAt + 1);
|
|
15318
|
+
}
|
|
15319
|
+
/**
|
|
15320
|
+
* The characters JS `\s` matches, which the scan below has to agree with to land on the
|
|
15321
|
+
* same match LOOSE_TEXT_ADDR would.
|
|
15322
|
+
*/
|
|
15323
|
+
function isSpaceCode(code) {
|
|
15324
|
+
return code === 32 || code >= 9 && code <= 13 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
|
|
15325
|
+
}
|
|
15326
|
+
/**
|
|
15327
|
+
* The characters JS `\w` matches without the unicode flag, the set the `\b` in
|
|
15328
|
+
* LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
|
|
15329
|
+
* which compares false throughout, so out of range reads as a non-word character.
|
|
15330
|
+
*/
|
|
15331
|
+
function isWordCode(code) {
|
|
15332
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
|
|
15333
|
+
}
|
|
15334
|
+
/**
|
|
15335
|
+
* Whether `\b` holds at an offset
|
|
15336
|
+
*/
|
|
15337
|
+
function isBoundary(text, at) {
|
|
15338
|
+
return isWordCode(text.charCodeAt(at - 1)) !== isWordCode(text.charCodeAt(at));
|
|
15339
|
+
}
|
|
15340
|
+
/**
|
|
15341
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
15342
|
+
* Bounded rather than indexOf, which would scan on to the end of the header once for every
|
|
15343
|
+
* whitespace delimited run and make the walk below quadratic.
|
|
15344
|
+
*/
|
|
15345
|
+
function indexOfAt(text, from, to) {
|
|
15346
|
+
for (let i = from; i < to; i++) if (text.charCodeAt(i) === 64) return i;
|
|
15347
|
+
return -1;
|
|
15348
|
+
}
|
|
15349
|
+
/**
|
|
15350
|
+
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
15351
|
+
*
|
|
15352
|
+
* Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
|
|
15353
|
+
* offset and rescans the run to the next '@' each time, so a single header well inside
|
|
15354
|
+
* the default size limit could hold a core busy for minutes. The search is also
|
|
15355
|
+
* unnecessary. '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin
|
|
15356
|
+
* at the head of a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b'
|
|
15357
|
+
* gives characters back until it lands on a boundary, so the only end it can take in that
|
|
15358
|
+
* run is the last boundary in it. Both are found in one pass, and the pattern is then run
|
|
15359
|
+
* at that single offset.
|
|
15360
|
+
*
|
|
15361
|
+
* @param text Free text to look in
|
|
15362
|
+
* @return Offset to match at, or -1
|
|
15363
|
+
*/
|
|
15364
|
+
function looseAddressStart(text) {
|
|
15365
|
+
const len = text.length;
|
|
15366
|
+
let pos = 0;
|
|
15367
|
+
while (pos < len) {
|
|
15368
|
+
while (pos < len && isSpaceCode(text.charCodeAt(pos))) pos++;
|
|
15369
|
+
if (pos >= len) break;
|
|
15370
|
+
const runStart = pos;
|
|
15371
|
+
let runEnd = pos;
|
|
15372
|
+
while (runEnd < len && !isSpaceCode(text.charCodeAt(runEnd))) runEnd++;
|
|
15373
|
+
let at = indexOfAt(text, runStart, runEnd);
|
|
15374
|
+
if (at >= 0) {
|
|
15375
|
+
let lastBoundary = -1;
|
|
15376
|
+
for (let k = runEnd; k > runStart; k--) if (isBoundary(text, k)) {
|
|
15377
|
+
lastBoundary = k;
|
|
15378
|
+
break;
|
|
15379
|
+
}
|
|
15380
|
+
let atomStart = runStart;
|
|
15381
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
15382
|
+
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
15383
|
+
for (let start = atomStart; start < at; start++) if (isBoundary(text, start)) {
|
|
15384
|
+
if (start > runStart) return start;
|
|
15385
|
+
let padded = runStart;
|
|
15386
|
+
while (padded > 0 && isSpaceCode(text.charCodeAt(padded - 1))) padded--;
|
|
15387
|
+
return padded;
|
|
15388
|
+
}
|
|
15389
|
+
}
|
|
15390
|
+
atomStart = at + 1;
|
|
15391
|
+
at = indexOfAt(text, atomStart, runEnd);
|
|
15392
|
+
}
|
|
15393
|
+
}
|
|
15394
|
+
pos = runEnd;
|
|
15395
|
+
}
|
|
15396
|
+
return -1;
|
|
15397
|
+
}
|
|
15398
|
+
/**
|
|
15399
|
+
* Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
|
|
15400
|
+
*
|
|
15401
|
+
* A malformed header can put more than a mailbox between the angle brackets, most often
|
|
15402
|
+
* because the generator wrote the recipient twice: '<user@example.com user@example.com>'
|
|
15403
|
+
* or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
|
|
15404
|
+
* never be a mailbox, and passing it on as the address loses the recipient that is
|
|
15405
|
+
* sitting right there in the header.
|
|
14572
15406
|
*
|
|
14573
|
-
*
|
|
14574
|
-
*
|
|
14575
|
-
*
|
|
14576
|
-
*
|
|
15407
|
+
* The run that still reads as an addr-spec is kept and whatever is left over becomes
|
|
15408
|
+
* display text. Candidates are read strictly first and then under the looser grammar,
|
|
15409
|
+
* the same two tiers the unquoted text fallback applies, so that '<a@b@c.com junk>' and a
|
|
15410
|
+
* bare 'a@b@c.com junk' agree on the recipient.
|
|
14577
15411
|
*
|
|
14578
|
-
*
|
|
14579
|
-
*
|
|
14580
|
-
* has to end on one.
|
|
15412
|
+
* A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
|
|
15413
|
+
* '<"user name"@example.com>' is well formed and means exactly what it says.
|
|
14581
15414
|
*
|
|
14582
|
-
* @param
|
|
14583
|
-
* @
|
|
15415
|
+
* @param address Collected address
|
|
15416
|
+
* @param text Collected display text
|
|
15417
|
+
* @return The address and the display text, with any leftovers moved to the text
|
|
14584
15418
|
*/
|
|
14585
|
-
function
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
if (
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
if (end <= at + 1) continue;
|
|
14597
|
-
return {
|
|
14598
|
-
index: match.index + start,
|
|
14599
|
-
length: end - start,
|
|
14600
|
-
value: token.substring(start, end)
|
|
15419
|
+
function recoverAddrSpec(address, text) {
|
|
15420
|
+
if (!HAS_WHITESPACE.test(address)) return {
|
|
15421
|
+
address,
|
|
15422
|
+
text
|
|
15423
|
+
};
|
|
15424
|
+
let rest;
|
|
15425
|
+
const quoted = address.match(QUOTED_LOCAL_ADDR);
|
|
15426
|
+
if (quoted) {
|
|
15427
|
+
if (!quoted[2]) return {
|
|
15428
|
+
address,
|
|
15429
|
+
text
|
|
14601
15430
|
};
|
|
15431
|
+
address = quoted[1];
|
|
15432
|
+
rest = [quoted[2]];
|
|
15433
|
+
} else {
|
|
15434
|
+
if (address.indexOf("\"") >= 0) return {
|
|
15435
|
+
address,
|
|
15436
|
+
text
|
|
15437
|
+
};
|
|
15438
|
+
const parts = address.split(/\s+/);
|
|
15439
|
+
let addrIndex = parts.findIndex((part) => ADDR_SPEC.test(part));
|
|
15440
|
+
if (addrIndex < 0) addrIndex = parts.findIndex((part) => LOOSE_ADDR_SPEC.test(part));
|
|
15441
|
+
if (addrIndex < 0) return {
|
|
15442
|
+
address,
|
|
15443
|
+
text
|
|
15444
|
+
};
|
|
15445
|
+
address = parts.splice(addrIndex, 1)[0];
|
|
15446
|
+
rest = parts;
|
|
14602
15447
|
}
|
|
14603
|
-
return
|
|
15448
|
+
return {
|
|
15449
|
+
address,
|
|
15450
|
+
text: [text, ...rest].filter((part) => part).join(" ")
|
|
15451
|
+
};
|
|
15452
|
+
}
|
|
15453
|
+
/**
|
|
15454
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
15455
|
+
*
|
|
15456
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
15457
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
15458
|
+
* put 'user@example.com(x)evil.com' into the address, a value that is no mailbox and that a
|
|
15459
|
+
* consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
|
|
15460
|
+
* whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
|
|
15461
|
+
* the same rule the token walk applies outside the brackets. Quoted strings and domain
|
|
15462
|
+
* literals are copied through untouched, a '(' in there is not a comment.
|
|
15463
|
+
*
|
|
15464
|
+
* @param address Contents of the angle brackets
|
|
15465
|
+
* @return The address with the comments removed, and the text of the comments
|
|
15466
|
+
*/
|
|
15467
|
+
function stripAddressComments(address) {
|
|
15468
|
+
const comments = [];
|
|
15469
|
+
let result = "";
|
|
15470
|
+
let comment = "";
|
|
15471
|
+
let depth = 0;
|
|
15472
|
+
let closer = "";
|
|
15473
|
+
let lastChar = "";
|
|
15474
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
15475
|
+
const chr = address.charAt(i);
|
|
15476
|
+
if (depth) {
|
|
15477
|
+
if (chr === "\\" && i < len - 1) comment += address.charAt(++i);
|
|
15478
|
+
else if (chr === "(") {
|
|
15479
|
+
depth++;
|
|
15480
|
+
comment += chr;
|
|
15481
|
+
} else if (chr === ")" && !--depth) {
|
|
15482
|
+
comments.push(comment.trim());
|
|
15483
|
+
comment = "";
|
|
15484
|
+
if (lastChar !== "@" && address.charAt(i + 1) !== "@") {
|
|
15485
|
+
result += " ";
|
|
15486
|
+
lastChar = " ";
|
|
15487
|
+
}
|
|
15488
|
+
} else comment += chr;
|
|
15489
|
+
continue;
|
|
15490
|
+
}
|
|
15491
|
+
if (closer) {
|
|
15492
|
+
if (chr === "\\" && closer === "\"" && i < len - 1) {
|
|
15493
|
+
result += chr + address.charAt(++i);
|
|
15494
|
+
lastChar = address.charAt(i);
|
|
15495
|
+
continue;
|
|
15496
|
+
}
|
|
15497
|
+
if (chr === closer) closer = "";
|
|
15498
|
+
} else if (chr === "\"") closer = "\"";
|
|
15499
|
+
else if (chr === "[") closer = "]";
|
|
15500
|
+
else if (chr === "(") {
|
|
15501
|
+
depth = 1;
|
|
15502
|
+
continue;
|
|
15503
|
+
}
|
|
15504
|
+
result += chr;
|
|
15505
|
+
lastChar = chr;
|
|
15506
|
+
}
|
|
15507
|
+
if (depth) comments.push(comment.trim());
|
|
15508
|
+
return {
|
|
15509
|
+
address: result.trim(),
|
|
15510
|
+
comments: comments.filter((text) => text)
|
|
15511
|
+
};
|
|
15512
|
+
}
|
|
15513
|
+
/**
|
|
15514
|
+
* Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
|
|
15515
|
+
* without other angle brackets that has an '@' with at least one character on each side.
|
|
15516
|
+
*
|
|
15517
|
+
* Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
|
|
15518
|
+
* followed by a long run of '@' and no closing `>`.
|
|
15519
|
+
*
|
|
15520
|
+
* @param text Text to check
|
|
15521
|
+
* @return true if an angle bracket address is present
|
|
15522
|
+
*/
|
|
15523
|
+
function hasAngleAddress(text) {
|
|
15524
|
+
let start = -1;
|
|
15525
|
+
let at = -1;
|
|
15526
|
+
for (let i = 0; i < text.length; i++) {
|
|
15527
|
+
const chr = text.charAt(i);
|
|
15528
|
+
if (chr === "<") {
|
|
15529
|
+
start = i + 1;
|
|
15530
|
+
at = -1;
|
|
15531
|
+
} else if (chr === ">") {
|
|
15532
|
+
if (at >= 0 && at < i - 1) return true;
|
|
15533
|
+
start = -1;
|
|
15534
|
+
at = -1;
|
|
15535
|
+
} else if (chr === "@" && start >= 0 && at < 0 && i > start) at = i;
|
|
15536
|
+
}
|
|
15537
|
+
return false;
|
|
14604
15538
|
}
|
|
14605
15539
|
/**
|
|
14606
15540
|
* Converts tokens for a single address into an address object
|
|
14607
15541
|
*
|
|
14608
|
-
* @param
|
|
14609
|
-
* @param
|
|
14610
|
-
* @return
|
|
15542
|
+
* @param tokens Tokens object
|
|
15543
|
+
* @param depth Current recursion depth for nested group protection
|
|
15544
|
+
* @return Address objects
|
|
14611
15545
|
*/
|
|
14612
15546
|
function _handleAddress(tokens, depth) {
|
|
14613
15547
|
let isGroup = false;
|
|
14614
15548
|
let state = "text";
|
|
14615
|
-
let address;
|
|
14616
15549
|
let addresses = [];
|
|
14617
15550
|
let data = {
|
|
14618
15551
|
address: [],
|
|
@@ -14624,6 +15557,12 @@ function _handleAddress(tokens, depth) {
|
|
|
14624
15557
|
let i;
|
|
14625
15558
|
let len;
|
|
14626
15559
|
let insideQuotes = false;
|
|
15560
|
+
const lastChars = {
|
|
15561
|
+
address: "",
|
|
15562
|
+
comment: "",
|
|
15563
|
+
group: "",
|
|
15564
|
+
text: ""
|
|
15565
|
+
};
|
|
14627
15566
|
for (i = 0, len = tokens.length; i < len; i++) {
|
|
14628
15567
|
let token = tokens[i];
|
|
14629
15568
|
let prevToken = i ? tokens[i - 1] : null;
|
|
@@ -14650,14 +15589,17 @@ function _handleAddress(tokens, depth) {
|
|
|
14650
15589
|
insideQuotes = false;
|
|
14651
15590
|
}
|
|
14652
15591
|
else if (token.value) {
|
|
15592
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
15593
|
+
const opensAfterEmptyQuotedString = prevToken?.type === "operator" && prevToken.value === "\"" && !!prevToken.noBreak && prevPrevToken?.type === "operator" && prevPrevToken.value === "\"";
|
|
14653
15594
|
if (state === "address") token.value = token.value.replace(/^[^<]*<\s*/, "");
|
|
14654
|
-
if (prevToken && prevToken.noBreak && data[state].length) {
|
|
15595
|
+
if (prevToken && prevToken.noBreak && data[state].length && (prevToken.value !== ")" || lastChars[state] === "@" || token.value.charAt(0) === "@")) {
|
|
14655
15596
|
data[state][data[state].length - 1] += token.value;
|
|
14656
15597
|
if (state === "text" && insideQuotes) data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
14657
15598
|
} else {
|
|
14658
15599
|
data[state].push(token.value);
|
|
14659
|
-
if (state === "text") data.textWasQuoted.push(insideQuotes);
|
|
15600
|
+
if (state === "text") data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
14660
15601
|
}
|
|
15602
|
+
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
14661
15603
|
}
|
|
14662
15604
|
}
|
|
14663
15605
|
if (!data.text.length && data.comment.length) {
|
|
@@ -14665,32 +15607,49 @@ function _handleAddress(tokens, depth) {
|
|
|
14665
15607
|
data.comment = [];
|
|
14666
15608
|
}
|
|
14667
15609
|
if (isGroup) {
|
|
14668
|
-
|
|
15610
|
+
const text = data.text.join(" ");
|
|
14669
15611
|
let groupMembers = [];
|
|
14670
|
-
if (data.group.length)
|
|
14671
|
-
if (member.group)
|
|
15612
|
+
if (data.group.length) parseAddressList$1(data.group.join(","), depth + 1).forEach((member) => {
|
|
15613
|
+
if (member.group) for (const nestedMember of member.group) groupMembers.push(nestedMember);
|
|
14672
15614
|
else groupMembers.push(member);
|
|
14673
15615
|
});
|
|
14674
15616
|
addresses.push({
|
|
14675
|
-
name: decodeWords(
|
|
15617
|
+
name: decodeWords(text),
|
|
14676
15618
|
group: groupMembers
|
|
14677
15619
|
});
|
|
14678
15620
|
} else {
|
|
15621
|
+
const addressComments = [];
|
|
15622
|
+
const addressParts = [];
|
|
15623
|
+
for (const part of data.address) {
|
|
15624
|
+
if (part.indexOf("(") < 0) {
|
|
15625
|
+
addressParts.push(part);
|
|
15626
|
+
continue;
|
|
15627
|
+
}
|
|
15628
|
+
const stripped = stripAddressComments(part);
|
|
15629
|
+
for (const comment of stripped.comments) addressComments.push(comment);
|
|
15630
|
+
if (stripped.address) addressParts.push(stripped.address);
|
|
15631
|
+
}
|
|
15632
|
+
data.address = addressParts;
|
|
14679
15633
|
if (!data.address.length && data.text.length) {
|
|
14680
|
-
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && data.text[i]
|
|
15634
|
+
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
|
|
14681
15635
|
data.address = data.text.splice(i, 1);
|
|
14682
15636
|
data.textWasQuoted.splice(i, 1);
|
|
14683
15637
|
break;
|
|
14684
15638
|
}
|
|
14685
15639
|
if (!data.address.length) {
|
|
14686
15640
|
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i]) {
|
|
14687
|
-
const
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
15641
|
+
const part = data.text[i];
|
|
15642
|
+
const start = looseAddressStart(part);
|
|
15643
|
+
if (start >= 0) {
|
|
15644
|
+
LOOSE_TEXT_ADDR.lastIndex = start;
|
|
15645
|
+
const match = LOOSE_TEXT_ADDR.exec(part);
|
|
15646
|
+
if (match) {
|
|
15647
|
+
data.address = [match[0].trim()];
|
|
15648
|
+
data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
|
|
15649
|
+
break;
|
|
15650
|
+
}
|
|
14692
15651
|
}
|
|
14693
|
-
data.text[i] =
|
|
15652
|
+
data.text[i] = part.trim();
|
|
14694
15653
|
}
|
|
14695
15654
|
}
|
|
14696
15655
|
}
|
|
@@ -14699,25 +15658,33 @@ function _handleAddress(tokens, depth) {
|
|
|
14699
15658
|
data.comment = [];
|
|
14700
15659
|
}
|
|
14701
15660
|
if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
|
|
14702
|
-
data.
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
15661
|
+
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
|
|
15662
|
+
let text = data.text.join(" ");
|
|
15663
|
+
let addressValue = data.address.join(" ");
|
|
15664
|
+
if (!addressValue && isEncodedWordsOnly(text.trim())) {
|
|
15665
|
+
const decodedText = decodeWords(text);
|
|
15666
|
+
if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
|
|
15667
|
+
const parsedSubAddresses = parseAddressList$1(decodedText, depth + 1);
|
|
15668
|
+
if (parsedSubAddresses.length) return parsedSubAddresses;
|
|
14709
15669
|
}
|
|
14710
15670
|
return [{
|
|
14711
15671
|
address: "",
|
|
14712
15672
|
name: decodedText
|
|
14713
15673
|
}];
|
|
14714
15674
|
}
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
15675
|
+
if (addressFromQuotedText && text.indexOf("@") >= 0) {
|
|
15676
|
+
addressValue = quoteLocalPart(text);
|
|
15677
|
+
text = "";
|
|
15678
|
+
}
|
|
15679
|
+
({address: addressValue, text} = recoverAddrSpec(addressValue, text));
|
|
15680
|
+
if (!text && addressComments.length) text = addressComments.join(" ");
|
|
15681
|
+
const rawName = text || addressValue || "";
|
|
15682
|
+
const address = {
|
|
15683
|
+
address: addressValue || text || "",
|
|
15684
|
+
name: decodeWords(rawName)
|
|
14718
15685
|
};
|
|
14719
|
-
if (address.address === address.name) {
|
|
14720
|
-
if (
|
|
15686
|
+
if (address.address === rawName || address.address === address.name) {
|
|
15687
|
+
if (address.address.match(/@/)) address.name = "";
|
|
14721
15688
|
else address.address = "";
|
|
14722
15689
|
}
|
|
14723
15690
|
addresses.push(address);
|
|
@@ -14725,22 +15692,21 @@ function _handleAddress(tokens, depth) {
|
|
|
14725
15692
|
return addresses;
|
|
14726
15693
|
}
|
|
14727
15694
|
/**
|
|
14728
|
-
*
|
|
14729
|
-
*
|
|
14730
|
-
* @constructor
|
|
14731
|
-
* @param {String} str Address field string
|
|
15695
|
+
* Tokenizer for address field strings
|
|
14732
15696
|
*/
|
|
14733
15697
|
var Tokenizer$2 = class {
|
|
15698
|
+
/**
|
|
15699
|
+
* @param str Address field string
|
|
15700
|
+
*/
|
|
14734
15701
|
constructor(str) {
|
|
14735
15702
|
this.str = (str || "").toString();
|
|
14736
15703
|
this.operatorCurrent = "";
|
|
14737
15704
|
this.operatorExpecting = "";
|
|
14738
15705
|
this.node = null;
|
|
14739
15706
|
this.escaped = false;
|
|
15707
|
+
this.inDomainLiteral = false;
|
|
15708
|
+
this.afterAt = false;
|
|
14740
15709
|
this.list = [];
|
|
14741
|
-
/**
|
|
14742
|
-
* Operator tokens and which tokens are expected to end the sequence
|
|
14743
|
-
*/
|
|
14744
15710
|
this.operators = {
|
|
14745
15711
|
"\"": "\"",
|
|
14746
15712
|
"(": ")",
|
|
@@ -14753,7 +15719,7 @@ var Tokenizer$2 = class {
|
|
|
14753
15719
|
/**
|
|
14754
15720
|
* Tokenizes the original input string
|
|
14755
15721
|
*
|
|
14756
|
-
* @return
|
|
15722
|
+
* @return An array of operator|text tokens
|
|
14757
15723
|
*/
|
|
14758
15724
|
tokenize() {
|
|
14759
15725
|
let list = [];
|
|
@@ -14771,9 +15737,14 @@ var Tokenizer$2 = class {
|
|
|
14771
15737
|
/**
|
|
14772
15738
|
* Checks if a character is an operator or text and acts accordingly
|
|
14773
15739
|
*
|
|
14774
|
-
* @param
|
|
15740
|
+
* @param chr Character from the address field
|
|
15741
|
+
* @param nextChr The character that follows, or null at the end of the field
|
|
14775
15742
|
*/
|
|
14776
15743
|
checkChar(chr, nextChr) {
|
|
15744
|
+
if (!this.escaped && !this.operatorExpecting) {
|
|
15745
|
+
if (!this.inDomainLiteral && chr === "[" && this.afterAt) this.inDomainLiteral = true;
|
|
15746
|
+
else if (this.inDomainLiteral && (chr === "]" || chr === "," || chr === ";")) this.inDomainLiteral = false;
|
|
15747
|
+
}
|
|
14777
15748
|
if (this.escaped) {} else if (chr === this.operatorExpecting) {
|
|
14778
15749
|
this.node = {
|
|
14779
15750
|
type: "operator",
|
|
@@ -14789,20 +15760,22 @@ var Tokenizer$2 = class {
|
|
|
14789
15760
|
].includes(nextChr)) this.node.noBreak = true;
|
|
14790
15761
|
this.list.push(this.node);
|
|
14791
15762
|
this.node = null;
|
|
15763
|
+
if (chr !== ")") this.afterAt = false;
|
|
14792
15764
|
this.operatorExpecting = "";
|
|
14793
15765
|
this.escaped = false;
|
|
14794
15766
|
return;
|
|
14795
|
-
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
15767
|
+
} else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ":") && chr in this.operators) {
|
|
14796
15768
|
this.node = {
|
|
14797
15769
|
type: "operator",
|
|
14798
15770
|
value: chr
|
|
14799
15771
|
};
|
|
14800
15772
|
this.list.push(this.node);
|
|
14801
15773
|
this.node = null;
|
|
15774
|
+
if (chr !== "(") this.afterAt = false;
|
|
14802
15775
|
this.operatorExpecting = this.operators[chr];
|
|
14803
15776
|
this.escaped = false;
|
|
14804
15777
|
return;
|
|
14805
|
-
} else if (this.operatorExpecting === "\"" && chr === "\\") {
|
|
15778
|
+
} else if ((this.operatorExpecting === "\"" || this.operatorExpecting === ")") && chr === "\\") {
|
|
14806
15779
|
this.escaped = true;
|
|
14807
15780
|
return;
|
|
14808
15781
|
}
|
|
@@ -14814,7 +15787,10 @@ var Tokenizer$2 = class {
|
|
|
14814
15787
|
this.list.push(this.node);
|
|
14815
15788
|
}
|
|
14816
15789
|
if (chr === "\n") chr = " ";
|
|
14817
|
-
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr))
|
|
15790
|
+
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
|
|
15791
|
+
this.node.value += chr;
|
|
15792
|
+
if (!this.operatorExpecting && chr !== " " && chr !== " ") this.afterAt = chr === "@";
|
|
15793
|
+
}
|
|
14818
15794
|
this.escaped = false;
|
|
14819
15795
|
}
|
|
14820
15796
|
};
|
|
@@ -14825,29 +15801,20 @@ var Tokenizer$2 = class {
|
|
|
14825
15801
|
*/
|
|
14826
15802
|
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
14827
15803
|
/**
|
|
14828
|
-
* Parses
|
|
14829
|
-
*
|
|
14830
|
-
*
|
|
14831
|
-
*
|
|
14832
|
-
* 'Name <address@domain>'
|
|
14833
|
-
*
|
|
14834
|
-
* will be converted to
|
|
14835
|
-
*
|
|
14836
|
-
* [{name: 'Name', address: 'address@domain'}]
|
|
15804
|
+
* Parses an address list, recursing into groups. The depth is threaded through the
|
|
15805
|
+
* calls rather than taken from an option, so a caller supplied options object can not
|
|
15806
|
+
* seed it and lift the recursion limit.
|
|
14837
15807
|
*
|
|
14838
|
-
* @param
|
|
14839
|
-
* @param
|
|
14840
|
-
* @
|
|
14841
|
-
* @return {Array} An array of address objects
|
|
15808
|
+
* @param str Address field
|
|
15809
|
+
* @param depth Current recursion depth for nested group protection
|
|
15810
|
+
* @return An array of address objects
|
|
14842
15811
|
*/
|
|
14843
|
-
function
|
|
14844
|
-
options = options || {};
|
|
14845
|
-
let depth = options._depth || 0;
|
|
15812
|
+
function parseAddressList$1(str, depth) {
|
|
14846
15813
|
if (depth > MAX_NESTED_GROUP_DEPTH) return [];
|
|
14847
15814
|
let tokens = new Tokenizer$2(str).tokenize();
|
|
14848
15815
|
let addresses = [];
|
|
14849
15816
|
let address = [];
|
|
14850
|
-
|
|
15817
|
+
const parsedAddresses = [];
|
|
14851
15818
|
tokens.forEach((token) => {
|
|
14852
15819
|
if (token.type === "operator" && (token.value === "," || token.value === ";")) {
|
|
14853
15820
|
if (address.length) addresses.push(address);
|
|
@@ -14855,11 +15822,34 @@ function addressParser(str, options) {
|
|
|
14855
15822
|
} else address.push(token);
|
|
14856
15823
|
});
|
|
14857
15824
|
if (address.length) addresses.push(address);
|
|
14858
|
-
addresses.
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
15825
|
+
for (const tokens of addresses) for (const address of _handleAddress(tokens, depth)) parsedAddresses.push(address);
|
|
15826
|
+
const mergedAddresses = [];
|
|
15827
|
+
for (let i = parsedAddresses.length - 1; i >= 0; i--) {
|
|
15828
|
+
const current = parsedAddresses[i];
|
|
15829
|
+
const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
|
|
15830
|
+
if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
|
|
15831
|
+
else mergedAddresses.push(current);
|
|
15832
|
+
}
|
|
15833
|
+
return mergedAddresses.reverse();
|
|
15834
|
+
}
|
|
15835
|
+
/**
|
|
15836
|
+
* Parses structured e-mail addresses from an address field
|
|
15837
|
+
*
|
|
15838
|
+
* Example:
|
|
15839
|
+
*
|
|
15840
|
+
* 'Name <address@domain>'
|
|
15841
|
+
*
|
|
15842
|
+
* will be converted to
|
|
15843
|
+
*
|
|
15844
|
+
* [{name: 'Name', address: 'address@domain'}]
|
|
15845
|
+
*
|
|
15846
|
+
* @param str Address field
|
|
15847
|
+
* @param options Optional options object
|
|
15848
|
+
* @return An array of address objects
|
|
15849
|
+
*/
|
|
15850
|
+
function addressParser(str, options) {
|
|
15851
|
+
const parsedAddresses = parseAddressList$1(str, 0);
|
|
15852
|
+
if (options && options.flatten) {
|
|
14863
15853
|
let addresses = [];
|
|
14864
15854
|
let walkAddressList = (list) => {
|
|
14865
15855
|
list.forEach((address) => {
|
|
@@ -14872,6 +15862,7 @@ function addressParser(str, options) {
|
|
|
14872
15862
|
}
|
|
14873
15863
|
return parsedAddresses;
|
|
14874
15864
|
}
|
|
15865
|
+
new Uint8Array([10]);
|
|
14875
15866
|
//#endregion
|
|
14876
15867
|
//#region ../../node_modules/.pnpm/domelementtype@3.0.0/node_modules/domelementtype/dist/index.js
|
|
14877
15868
|
/** Types of elements found in htmlparser2's DOM */
|
|
@@ -25992,7 +26983,7 @@ function bad(message, hint) {
|
|
|
25992
26983
|
/** A bare address, checked and canonicalised. Display names are only accepted on From. */
|
|
25993
26984
|
function address(value, field) {
|
|
25994
26985
|
const trimmed = value.trim();
|
|
25995
|
-
if (!ADDRESS.test(trimmed)) throw bad(`${field}
|
|
26986
|
+
if (!ADDRESS.test(trimmed)) throw bad(`${field} contains a value that is not a bare email address`, "Give bare addresses.");
|
|
25996
26987
|
return canonicalAddress(trimmed);
|
|
25997
26988
|
}
|
|
25998
26989
|
function list(values, field) {
|
|
@@ -27778,11 +28769,37 @@ function outcomeUnavailable(why) {
|
|
|
27778
28769
|
//#endregion
|
|
27779
28770
|
//#region src/operations/send.ts
|
|
27780
28771
|
/**
|
|
28772
|
+
* A caller may hand back the canonical expectation it kept, or the display-safe `expect` that prepare returned. The
|
|
28773
|
+
* latter is accepted only when it is byte-for-byte this approval's field rendering under the boundary it carries, the
|
|
28774
|
+
* way Gmail accepts its own; then the claim compares the raw binding. Anything else is passed on as given, so an
|
|
28775
|
+
* invented or edited envelope still mismatches and the send is refused.
|
|
28776
|
+
*/
|
|
28777
|
+
function canonicalSuppliedExpectation(supplied, bound, account, id) {
|
|
28778
|
+
const boundary = /^<untrusted-content boundary="([A-Za-z0-9_-]+)" field="subject"/.exec(supplied.subject)?.[1];
|
|
28779
|
+
if (boundary === void 0) return supplied;
|
|
28780
|
+
const displayed = expectationFields(bound, {
|
|
28781
|
+
boundary,
|
|
28782
|
+
account,
|
|
28783
|
+
id
|
|
28784
|
+
});
|
|
28785
|
+
const same = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
28786
|
+
return displayed.subject === supplied.subject && same(displayed.to, supplied.to) && same(displayed.cc, supplied.cc) && same(displayed.bcc, supplied.bcc) ? bound : supplied;
|
|
28787
|
+
}
|
|
28788
|
+
/**
|
|
27781
28789
|
* A used approval, as Resend's own surfaces refuse it (design 2026-10-05 §D2, the used-send row): Resend accepted the
|
|
27782
28790
|
* email — which is not that it went, since one may be scheduled, bounce or be cancelled — and never "sent". The words
|
|
27783
28791
|
* `resend_send_wait` shows for it too, core's for every provider.
|
|
27784
28792
|
*/
|
|
27785
28793
|
const ACCEPTED_BY_RESEND = (usedAt) => `accepted by Resend at ${usedAt}`;
|
|
28794
|
+
/** A send's recipients and subject as Resend may return them: display-safe, never the raw approval binding. */
|
|
28795
|
+
function expectationFields(expect, envelope) {
|
|
28796
|
+
return {
|
|
28797
|
+
to: expect.to.map((address) => addressField(address, "to", envelope)),
|
|
28798
|
+
cc: expect.cc.map((address) => addressField(address, "cc", envelope)),
|
|
28799
|
+
bcc: expect.bcc.map((address) => addressField(address, "bcc", envelope)),
|
|
28800
|
+
subject: wrapField(expect.subject, "subject", envelope)
|
|
28801
|
+
};
|
|
28802
|
+
}
|
|
27786
28803
|
/**
|
|
27787
28804
|
* A send's approval as the one account that may use it, classified under its lock as the claim would classify it
|
|
27788
28805
|
* (design 2026-10-05 §D2), or a refusal that does not touch it.
|
|
@@ -28043,9 +29060,13 @@ async function prepareSend(context, name, input) {
|
|
|
28043
29060
|
effectivePolicy: effective,
|
|
28044
29061
|
riskFlags: study.flags,
|
|
28045
29062
|
reach,
|
|
28046
|
-
expect: record.expect,
|
|
29063
|
+
expect: expectationFields(record.expect, {
|
|
29064
|
+
boundary: newBoundary(),
|
|
29065
|
+
account: name,
|
|
29066
|
+
id: record.approvalId
|
|
29067
|
+
}),
|
|
28047
29068
|
expiresAt: record.expiresAt,
|
|
28048
|
-
nextStep: effective === "confirm" ? `${approveAndWaitSentence(context.handoffs, context.surface, record.approvalId, (command, wait) => `Show the preview to the user, then have them run ${command} in their own terminal${wait === void 0 ? "" : `; learn when they have with ${wait}`}.`, { instead: "Show the preview to the user; they approve it at their own terminal." })} You cannot approve this yourself. Then execute it with the same approval id
|
|
29069
|
+
nextStep: effective === "confirm" ? `${approveAndWaitSentence(context.handoffs, context.surface, record.approvalId, (command, wait) => `Show the preview to the user, then have them run ${command} in their own terminal${wait === void 0 ? "" : `; learn when they have with ${wait}`}.`, { instead: "Show the preview to the user; they approve it at their own terminal." })} You cannot approve this yourself. Then execute it with the same approval id.` : "Show the preview to the user verbatim and wait for an explicit yes. Then execute it with the same approval id.",
|
|
28049
29070
|
approval: await context.core.approvals.approvalOf(record),
|
|
28050
29071
|
...legacyDrain === void 0 ? {} : { legacyDrain }
|
|
28051
29072
|
};
|
|
@@ -28236,7 +29257,7 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
|
|
|
28236
29257
|
contentDigest: digest,
|
|
28237
29258
|
inboxId: named.account.id,
|
|
28238
29259
|
inboxSub: named.account.userId,
|
|
28239
|
-
expect: options.expect
|
|
29260
|
+
expect: options.expect === void 0 ? known.expect : canonicalSuppliedExpectation(options.expect, known.expect, name, known.approvalId)
|
|
28240
29261
|
}, {
|
|
28241
29262
|
pendingHint: `${approveAndWaitSentence(context.handoffs, context.surface, options.approvalId, (command, wait) => `Ask the user to run ${command} in their own terminal${wait === void 0 ? "," : `; learn when they have with ${wait},`} then execute it again with the same approval.`, { instead: "Ask the user to approve it at their own terminal, then execute it again with the same approval." })} You cannot approve it yourself.`,
|
|
28242
29263
|
usedSaid: ACCEPTED_BY_RESEND,
|
|
@@ -28396,16 +29417,26 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
|
|
|
28396
29417
|
state: message.scheduledAt ? "scheduled" : "sent",
|
|
28397
29418
|
scheduledAt: message.scheduledAt,
|
|
28398
29419
|
said: acceptedSaid(resendId !== void 0, message.scheduledAt),
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
29420
|
+
...expectationFields(claimed.expect, {
|
|
29421
|
+
boundary: newBoundary(),
|
|
29422
|
+
account: name,
|
|
29423
|
+
id: options.approvalId
|
|
29424
|
+
}),
|
|
28403
29425
|
...unrecorded.length > 0 ? { note: unrecorded.join("; ") } : {},
|
|
28404
29426
|
approval: approval ?? await approvalNow(context, options.approvalId, named.account.id, claimedApproval),
|
|
28405
29427
|
...legacyDrain === void 0 ? {} : { legacyDrain }
|
|
28406
29428
|
};
|
|
28407
29429
|
});
|
|
28408
29430
|
}
|
|
29431
|
+
/** A stored send as a result may show it: canonical recipients stay addresses; its subject is always prose. */
|
|
29432
|
+
function sendSummaryFields(summary, envelope) {
|
|
29433
|
+
if (summary === null) return null;
|
|
29434
|
+
return {
|
|
29435
|
+
...summary,
|
|
29436
|
+
recipients: summary.recipients.map((address) => addressField(address, "recipient", envelope)),
|
|
29437
|
+
...summary.subject === void 0 ? {} : { subject: wrapField(summary.subject, "subject", envelope) }
|
|
29438
|
+
};
|
|
29439
|
+
}
|
|
28409
29440
|
const CANNOT_READ = "this key can only send, so it cannot read what Resend did with the email";
|
|
28410
29441
|
/**
|
|
28411
29442
|
* What this machine knows Resend accepted, from its own send record: a scheduled send is accepted for the time the
|
|
@@ -28439,11 +29470,16 @@ async function sendStatus(context, name, approvalId) {
|
|
|
28439
29470
|
throw error;
|
|
28440
29471
|
});
|
|
28441
29472
|
const stored = seen?.stored ?? null;
|
|
29473
|
+
const envelope = {
|
|
29474
|
+
boundary: newBoundary(),
|
|
29475
|
+
account: name,
|
|
29476
|
+
id: approvalId
|
|
29477
|
+
};
|
|
28442
29478
|
const base = {
|
|
28443
29479
|
account: name,
|
|
28444
29480
|
approvalId,
|
|
28445
|
-
approval: seen ? publicApproval(seen.stored, seen.outcome) : null,
|
|
28446
|
-
local
|
|
29481
|
+
approval: seen ? publicApproval(seen.stored, seen.outcome, { wrap: (text, field) => field === "subject" || field === "filename" ? wrapField(text, field, envelope) : addressField(text, field, envelope) }) : null,
|
|
29482
|
+
local: sendSummaryFields(local, envelope)
|
|
28447
29483
|
};
|
|
28448
29484
|
if (!local) return {
|
|
28449
29485
|
...base,
|