@agentcomms/resend 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-D-SXJezp.mjs} +1255 -228
- package/dist/{server-xhrQV9qo.mjs → server-BZOLLDTu.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-DcVZBKOq.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.0";
|
|
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-C_PW2LlM.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
|
|
@@ -4395,7 +4784,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
|
|
|
4395
4784
|
runtimeDir: join(context.core.paths.dataDir, "runtime"),
|
|
4396
4785
|
dryRun: options.dryRun === true,
|
|
4397
4786
|
removed: [],
|
|
4398
|
-
kept: []
|
|
4787
|
+
kept: [],
|
|
4788
|
+
shims: {
|
|
4789
|
+
removed: [],
|
|
4790
|
+
kept: []
|
|
4791
|
+
}
|
|
4399
4792
|
};
|
|
4400
4793
|
const candidates = [];
|
|
4401
4794
|
for (const { path, version } of await listManagedRuntimes(context.core.paths.dataDir, product.packageName)) {
|
|
@@ -4462,6 +4855,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
|
|
|
4462
4855
|
const left = handedOut.filter((record) => !gone.some((path) => mentions([path], record.runtime)));
|
|
4463
4856
|
if (left.length !== handedOut.length) await writeFileAtomic(ledger, left.map((record) => `${JSON.stringify(record)}\n`).join(""));
|
|
4464
4857
|
}
|
|
4858
|
+
if (result.removed.length > 0) result.shims = await removeManagedShimsForRuntimes({
|
|
4859
|
+
env: context.env,
|
|
4860
|
+
dataDir: context.core.paths.dataDir,
|
|
4861
|
+
platform: context.platform
|
|
4862
|
+
}, result.removed.map((item) => item.path), { dryRun: result.dryRun });
|
|
4465
4863
|
return result;
|
|
4466
4864
|
}
|
|
4467
4865
|
/** Every runtime the installer handed out, or null when the record is there and cannot be read. */
|
|
@@ -4594,6 +4992,8 @@ function rivalWordWarnings(servers, word, can, product) {
|
|
|
4594
4992
|
}
|
|
4595
4993
|
/** Every channel in core's snapshot, the core first. */
|
|
4596
4994
|
const CHANNELS = Object.freeze(CHANNEL_SNAPSHOT.map((entry) => entry.manifest.channel));
|
|
4995
|
+
/** Every declared MCP server, channels first and then services, with no separate hand-kept list. */
|
|
4996
|
+
const SERVERS = Object.freeze([...CHANNELS, ...SERVICE_SNAPSHOT.map((entry) => entry.declaration.server.defaultName)]);
|
|
4597
4997
|
/**
|
|
4598
4998
|
* The value a pin's flag carries in an entry's arguments: `--account acme/resend`, or `--account=acme/resend` — the
|
|
4599
4999
|
* first given, in either form.
|
|
@@ -4654,8 +5054,28 @@ function serverFactsOf({ packageName, manifest }) {
|
|
|
4654
5054
|
};
|
|
4655
5055
|
}
|
|
4656
5056
|
const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
|
|
5057
|
+
/** Service server facts, derived from each service package's declaration. */
|
|
5058
|
+
const SERVICE_SERVERS = Object.freeze(Object.fromEntries(SERVICE_SNAPSHOT.map(({ packageName, declaration }) => [declaration.server.defaultName, {
|
|
5059
|
+
packageName,
|
|
5060
|
+
binary: declaration.binary,
|
|
5061
|
+
defaultServerName: declaration.server.defaultName,
|
|
5062
|
+
npxPackage: packageName,
|
|
5063
|
+
npxArgs: ["mcp"],
|
|
5064
|
+
serverArgs: () => [],
|
|
5065
|
+
narrowingOf: () => ({})
|
|
5066
|
+
}])));
|
|
5067
|
+
/** All managed servers, derived from channel manifests and service declarations. */
|
|
5068
|
+
const SERVERS_BY_ID = Object.freeze({
|
|
5069
|
+
...CHANNEL_SERVERS,
|
|
5070
|
+
...SERVICE_SERVERS
|
|
5071
|
+
});
|
|
4657
5072
|
/** How each server is named to a person: in a preview, and in what a tool returns. */
|
|
4658
5073
|
const CHANNEL_LABELS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, entry.manifest.label])));
|
|
5074
|
+
/** Labels for every managed server; a service declares its public server name. */
|
|
5075
|
+
const SERVER_LABELS = Object.freeze({
|
|
5076
|
+
...CHANNEL_LABELS,
|
|
5077
|
+
...Object.fromEntries(SERVICE_SNAPSHOT.map(({ declaration }) => [declaration.server.defaultName, declaration.server.defaultName]))
|
|
5078
|
+
});
|
|
4659
5079
|
/** A channel's manifest, from core's snapshot. */
|
|
4660
5080
|
function channelManifest(channel) {
|
|
4661
5081
|
return CHANNEL_SNAPSHOT.find((entry) => entry.manifest.channel === channel)?.manifest;
|
|
@@ -4663,10 +5083,18 @@ function channelManifest(channel) {
|
|
|
4663
5083
|
function isChannel(value) {
|
|
4664
5084
|
return typeof value === "string" && CHANNELS.includes(value);
|
|
4665
5085
|
}
|
|
5086
|
+
/** Whether a word identifies any channel or service server in this release. */
|
|
5087
|
+
function isServer(value) {
|
|
5088
|
+
return typeof value === "string" && SERVERS.includes(value);
|
|
5089
|
+
}
|
|
4666
5090
|
/** Refuses a word that is not a channel of this release, naming the ones that are. */
|
|
4667
5091
|
function notAChannel(value) {
|
|
4668
5092
|
return new CommsError("USAGE", `"${String(value)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
4669
5093
|
}
|
|
5094
|
+
/** Refuses a word that is not a declared server of this release. */
|
|
5095
|
+
function notAServer(value) {
|
|
5096
|
+
return new CommsError("USAGE", `"${String(value)}" is not a server`, { hint: `One of: ${SERVERS.join(", ")}.` });
|
|
5097
|
+
}
|
|
4670
5098
|
/** A channel's server facts, by its word; refused for a word that is not a channel. */
|
|
4671
5099
|
function channelServer(channel) {
|
|
4672
5100
|
if (!isChannel(channel)) throw notAChannel(channel);
|
|
@@ -4677,6 +5105,16 @@ function channelLabel(channel) {
|
|
|
4677
5105
|
if (!isChannel(channel)) throw notAChannel(channel);
|
|
4678
5106
|
return CHANNEL_LABELS[channel];
|
|
4679
5107
|
}
|
|
5108
|
+
/** A channel or service's server facts, by its declared server name. */
|
|
5109
|
+
function serverFacts(server) {
|
|
5110
|
+
if (!isServer(server)) throw notAServer(server);
|
|
5111
|
+
return SERVERS_BY_ID[server];
|
|
5112
|
+
}
|
|
5113
|
+
/** How a channel or service server is named to a person. */
|
|
5114
|
+
function serverLabel(server) {
|
|
5115
|
+
if (!isServer(server)) throw notAServer(server);
|
|
5116
|
+
return SERVER_LABELS[server];
|
|
5117
|
+
}
|
|
4680
5118
|
/** A channel's manifest, by its word; refused for a word that is not a channel. */
|
|
4681
5119
|
function requireChannelManifest(channel) {
|
|
4682
5120
|
const manifest = channelManifest(channel);
|
|
@@ -9643,7 +10081,7 @@ function folding(platform) {
|
|
|
9643
10081
|
return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
|
|
9644
10082
|
}
|
|
9645
10083
|
/** The package version, read from package.json at build time. */
|
|
9646
|
-
const VERSION = "0.
|
|
10084
|
+
const VERSION = "0.18.0";
|
|
9647
10085
|
/**
|
|
9648
10086
|
* Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
|
|
9649
10087
|
* to decide whether the release it last heard of is newer than the one running.
|
|
@@ -9779,15 +10217,18 @@ var PrintedCommand = class {
|
|
|
9779
10217
|
#entry;
|
|
9780
10218
|
/** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
|
|
9781
10219
|
words;
|
|
9782
|
-
/** The line to paste; null when
|
|
10220
|
+
/** The line to paste; null when no supported common or labelled shell rendering is safe. */
|
|
9783
10221
|
line;
|
|
9784
10222
|
platform;
|
|
10223
|
+
shell;
|
|
9785
10224
|
constructor(gate, words, platform, entry) {
|
|
9786
10225
|
if (gate !== GATE) throw new TypeError("a printed command is made by locateCliCommand, and only there");
|
|
9787
10226
|
this.#entry = entry;
|
|
9788
10227
|
this.words = Object.freeze([...words]);
|
|
9789
|
-
|
|
10228
|
+
const quoted = quoteCommand(this.words, platform);
|
|
10229
|
+
this.line = quoted.line;
|
|
9790
10230
|
this.platform = platform;
|
|
10231
|
+
if (quoted.shell !== void 0) this.shell = quoted.shell;
|
|
9791
10232
|
Object.freeze(this);
|
|
9792
10233
|
}
|
|
9793
10234
|
/**
|
|
@@ -9814,7 +10255,7 @@ const STRIP_TYPES = "--experimental-strip-types";
|
|
|
9814
10255
|
const TRANSFORM_TYPES = "--experimental-transform-types";
|
|
9815
10256
|
/** The suite's packages, by name, and its channel packages, which reach core through their own dependency on it. */
|
|
9816
10257
|
const SUITE_PACKAGES = new Set(CHANNEL_SNAPSHOT.map(({ packageName }) => packageName));
|
|
9817
|
-
const CHANNEL_PACKAGES
|
|
10258
|
+
const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
|
|
9818
10259
|
/**
|
|
9819
10260
|
* Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
|
|
9820
10261
|
*
|
|
@@ -9897,7 +10338,7 @@ function locateCliCommand(request, runtime = {
|
|
|
9897
10338
|
sameCheckout: true
|
|
9898
10339
|
});
|
|
9899
10340
|
}
|
|
9900
|
-
if (target.manifest.channel === "core" && CHANNEL_PACKAGES
|
|
10341
|
+
if (target.manifest.channel === "core" && CHANNEL_PACKAGES.has(caller.packageName)) {
|
|
9901
10342
|
const installed = installedPackage(dirname(callerFile), target.packageName);
|
|
9902
10343
|
if (installed === null) return fail("core", `${caller.packageName} has no ${target.packageName} installed where it can find one`);
|
|
9903
10344
|
const manifest = readPackageManifest(installed);
|
|
@@ -9925,7 +10366,9 @@ function locateCliCommand(request, runtime = {
|
|
|
9925
10366
|
}
|
|
9926
10367
|
if (!SUITE_PACKAGES.has(caller.packageName)) return fail("direction", `${caller.packageName} is not a package of this suite`);
|
|
9927
10368
|
if (request.registrations === void 0) return fail("no-registrations", "nothing registered was given to look through for another product's command");
|
|
9928
|
-
const
|
|
10369
|
+
const channelTarget = CHANNEL_SNAPSHOT.find((entry) => entry.packageName === target.packageName);
|
|
10370
|
+
if (channelTarget === void 0) return fail("direction", `${target.packageName} is not a channel package this locator can find through a registration`);
|
|
10371
|
+
const facts = registrationFactsOf(channelTarget);
|
|
9929
10372
|
const notUsed = [];
|
|
9930
10373
|
const usable = [];
|
|
9931
10374
|
let engineRange;
|
|
@@ -10023,8 +10466,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
|
|
|
10023
10466
|
let entry;
|
|
10024
10467
|
if (found.source) entry = join(found.root, "src", "cli.ts");
|
|
10025
10468
|
else {
|
|
10026
|
-
const
|
|
10027
|
-
const binary = typeof declared === "object" && declared !== null ? declared.binary : void 0;
|
|
10469
|
+
const binary = declaredBinary(found.manifest);
|
|
10028
10470
|
const bin = found.manifest.bin;
|
|
10029
10471
|
const path = binary === target.manifest.binary && typeof bin === "object" && bin !== null && Object.hasOwn(bin, target.manifest.binary) ? bin[target.manifest.binary] : void 0;
|
|
10030
10472
|
if (typeof path !== "string" || path.length === 0) return {
|
|
@@ -10053,6 +10495,16 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
|
|
|
10053
10495
|
};
|
|
10054
10496
|
}
|
|
10055
10497
|
/**
|
|
10498
|
+
* The command a package declares for itself: a channel's in its `agentcomms` field, a service's in `agentcommsPackage`
|
|
10499
|
+
* (D14). A library declares none, and no package declares both: the registry refuses one that does.
|
|
10500
|
+
*/
|
|
10501
|
+
function declaredBinary(manifest) {
|
|
10502
|
+
const channel = manifest.agentcomms;
|
|
10503
|
+
if (typeof channel === "object" && channel !== null) return channel.binary;
|
|
10504
|
+
const service = manifest.agentcommsPackage;
|
|
10505
|
+
if (typeof service === "object" && service !== null && service.kind === "service") return service.binary;
|
|
10506
|
+
}
|
|
10507
|
+
/**
|
|
10056
10508
|
* The target's package a registration's files lead to, or null: a script it starts, real or as written when it is
|
|
10057
10509
|
* gone, inside the package (or inside the product's own server wrapper, which depends on it); or, for the product's
|
|
10058
10510
|
* own command, the package that command links into or that npm installed beside it.
|
|
@@ -10184,7 +10636,7 @@ function notLocated(target, version, reason, detail, nodeVersion, extra) {
|
|
|
10184
10636
|
* anywhere else for the product, PATH included, and a later locate checks the path again.
|
|
10185
10637
|
*/
|
|
10186
10638
|
function checkedEntry(root, entry) {
|
|
10187
|
-
if (!isWithin(root, entry)) return {
|
|
10639
|
+
if (!isWithin$1(root, entry)) return {
|
|
10188
10640
|
ok: false,
|
|
10189
10641
|
why: `${entry} is outside its package, ${root}`
|
|
10190
10642
|
};
|
|
@@ -10197,7 +10649,7 @@ function checkedEntry(root, entry) {
|
|
|
10197
10649
|
why: `${entry} is not there`
|
|
10198
10650
|
};
|
|
10199
10651
|
}
|
|
10200
|
-
if (!isWithin(root, real)) return {
|
|
10652
|
+
if (!isWithin$1(root, real)) return {
|
|
10201
10653
|
ok: false,
|
|
10202
10654
|
why: `${entry} leads to ${real}, outside its package, ${root}`
|
|
10203
10655
|
};
|
|
@@ -10611,6 +11063,10 @@ function renderInstall(result, color) {
|
|
|
10611
11063
|
lines.push(paint(color, "bold", `Add this to ${result.configPath ?? `the MCP configuration of ${result.client}`}:`), result.snippet.trimEnd());
|
|
10612
11064
|
}
|
|
10613
11065
|
if (result.backupPath) lines.push(`The entry it replaced was saved to ${result.backupPath}.`);
|
|
11066
|
+
if (result.shim && result.shim.status !== "foreign") lines.push(`CLI shim ${result.shim.status}: ${result.shim.path}.`);
|
|
11067
|
+
if (result.shimFailure) lines.push(paint(color, "yellow", `CLI shim failed: ${result.shimFailure}.`));
|
|
11068
|
+
if (result.shimNotice) lines.push(paint(color, "yellow", result.shimNotice));
|
|
11069
|
+
if (result.shim?.pathNotice) lines.push(result.shim.pathNotice);
|
|
10614
11070
|
if (result.verification === "passed") lines.push(paint(color, "green", `Checked: ${result.verifyDetail}`));
|
|
10615
11071
|
else if (result.verification === "failed") {
|
|
10616
11072
|
lines.push(paint(color, "red", `Failed to start: ${result.verifyDetail ?? "no reason given"}`));
|
|
@@ -10625,6 +11081,8 @@ function renderPrune(result, color) {
|
|
|
10625
11081
|
if (result.refused) lines.push(paint(color, "yellow", `Nothing removed: ${result.refused}.`));
|
|
10626
11082
|
const verb = result.dryRun ? "Would remove" : "Removed";
|
|
10627
11083
|
for (const item of result.removed) lines.push(paint(color, "green", `${verb} ${item.version}: ${item.path}`));
|
|
11084
|
+
for (const path of result.shims.removed) lines.push(paint(color, "green", `${verb} CLI shim: ${path}`));
|
|
11085
|
+
for (const path of result.shims.kept) lines.push(`Kept foreign CLI shim: ${path}`);
|
|
10628
11086
|
for (const item of result.kept) lines.push(`Kept ${item.version} (${item.reason}): ${item.path}`);
|
|
10629
11087
|
if (lines.length === 0) lines.push(`No managed runtimes to remove in ${result.runtimeDir}.`);
|
|
10630
11088
|
return lines.join("\n");
|
|
@@ -10636,7 +11094,7 @@ function describeItem(item) {
|
|
|
10636
11094
|
const where = item.scope === "project" ? `for a project, in ${item.path}` : `in ${item.path}`;
|
|
10637
11095
|
const pins = item.narrowing.length > 0 ? `, ${item.narrowing.join(" ")}` : "";
|
|
10638
11096
|
const version = item.version === null ? "pins no release" : `${item.version}${item.version === item.latest ? "" : ` → ${item.latest}`}`;
|
|
10639
|
-
return `${
|
|
11097
|
+
return `${serverLabel(item.channel)} with ${item.client} as "${item.name}" ${where} (${item.launcher}, ${version}${pins})`;
|
|
10640
11098
|
}
|
|
10641
11099
|
function renderUpdate(result, color) {
|
|
10642
11100
|
const lines = [];
|
|
@@ -10648,6 +11106,8 @@ function renderUpdate(result, color) {
|
|
|
10648
11106
|
if (step.verification === "passed") lines.push(` Checked: ${step.detail}`);
|
|
10649
11107
|
else if (step.verification === "failed") lines.push(paint(color, "red", ` Failed to start: ${step.detail}`));
|
|
10650
11108
|
else lines.push(paint(color, "yellow", ` Not checked: ${step.detail ?? "skipped"}`));
|
|
11109
|
+
if (step.shim && step.shim.status !== "foreign") lines.push(` CLI shim ${step.shim.status}: ${step.shim.path}.`);
|
|
11110
|
+
if (step.shim?.pathNotice) lines.push(` ${step.shim.pathNotice}`);
|
|
10651
11111
|
for (const warning of step.warnings ?? []) lines.push(` ${warning}`);
|
|
10652
11112
|
} else lines.push(paint(color, "red", `${step.outcome === "skipped" ? "Skipped" : "Could not register"} ${what}: ${step.detail}`));
|
|
10653
11113
|
} 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 +12512,10 @@ async function npmInstallGlobal(env, spec) {
|
|
|
12052
12512
|
}
|
|
12053
12513
|
}
|
|
12054
12514
|
/**
|
|
12055
|
-
* Registering, pruning and listing
|
|
12515
|
+
* Registering, pruning and listing every declared channel or service MCP server, from the core.
|
|
12056
12516
|
*
|
|
12057
|
-
* `agentcomms mcp install` registers
|
|
12058
|
-
*
|
|
12517
|
+
* `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
|
|
12518
|
+
* server; and channel CLIs register their own. They are the one change here, over
|
|
12059
12519
|
* the shared installer (`mcp-install.ts`), and all go through the change flow: registering a server is a loosening in
|
|
12060
12520
|
* 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
12521
|
* second call with the approval. Printing an entry registers nothing and asks nobody.
|
|
@@ -12066,7 +12526,7 @@ async function npmInstallGlobal(env, spec) {
|
|
|
12066
12526
|
* the other — or worse, a channel's own command would register with no approval at all, which is what it did.
|
|
12067
12527
|
*
|
|
12068
12528
|
* 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
|
|
12529
|
+
* released together, at one version, so the core asked to install Gmail or events installs its own release.
|
|
12070
12530
|
*/
|
|
12071
12531
|
const CLIENTS = Object.freeze([
|
|
12072
12532
|
"claude-code",
|
|
@@ -12107,10 +12567,10 @@ const exists = (path) => access(path, constants.R_OK).then(() => true, () => fal
|
|
|
12107
12567
|
* core depends on none of them. So the channel is looked for beside this core — `packages/gmail` next to
|
|
12108
12568
|
* `packages/core` in a checkout — and from source when this core runs from source, as the channel CLIs do.
|
|
12109
12569
|
*/
|
|
12110
|
-
async function localModuleUrl(
|
|
12570
|
+
async function localModuleUrl(server) {
|
|
12111
12571
|
const coreRoot = await corePackageRoot();
|
|
12112
|
-
const unscopedName =
|
|
12113
|
-
const root =
|
|
12572
|
+
const unscopedName = serverFacts(server).packageName.split("/").at(-1) ?? server;
|
|
12573
|
+
const root = server === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
|
|
12114
12574
|
const source = join(root, "src", "cli.ts");
|
|
12115
12575
|
const built = join(root, "dist", "cli.mjs");
|
|
12116
12576
|
const fromSource = fileURLToPath(import.meta.url).endsWith(".ts");
|
|
@@ -12118,7 +12578,7 @@ async function localModuleUrl(channel) {
|
|
|
12118
12578
|
if (!await exists(cli)) continue;
|
|
12119
12579
|
return pathToFileURL(cli === source ? join(root, "src", "mcp", "module.ts") : join(root, "dist", "module.mjs")).href;
|
|
12120
12580
|
}
|
|
12121
|
-
throw new CommsError("USAGE", `\`--launcher local\` registers a checkout's own code, and there is no ${
|
|
12581
|
+
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
12582
|
}
|
|
12123
12583
|
/**
|
|
12124
12584
|
* The product the shared installer registers, for one channel: at this core's version unless another is named.
|
|
@@ -12128,11 +12588,11 @@ async function localModuleUrl(channel) {
|
|
|
12128
12588
|
* newer release here is all it takes to register one this core is older than — which is what `agentcomms update` does.
|
|
12129
12589
|
* `comms_server_install` and every `mcp install` name none, and register their own release as they always have.
|
|
12130
12590
|
*/
|
|
12131
|
-
async function
|
|
12591
|
+
async function serverProduct(server, launcher, version = VERSION) {
|
|
12132
12592
|
return {
|
|
12133
|
-
...
|
|
12593
|
+
...serverFacts(server),
|
|
12134
12594
|
version,
|
|
12135
|
-
moduleUrl: launcher === "local" ? await localModuleUrl(
|
|
12595
|
+
moduleUrl: launcher === "local" ? await localModuleUrl(server) : ""
|
|
12136
12596
|
};
|
|
12137
12597
|
}
|
|
12138
12598
|
/**
|
|
@@ -12145,8 +12605,8 @@ async function channelProduct(channel, launcher, version = VERSION) {
|
|
|
12145
12605
|
* registered, from the channel; a product for another package would register something else under those words, and
|
|
12146
12606
|
* the person would have approved a sentence that was not true.
|
|
12147
12607
|
*/
|
|
12148
|
-
function ownProduct(
|
|
12149
|
-
if (own !== void 0 && own.packageName !==
|
|
12608
|
+
function ownProduct(server, own) {
|
|
12609
|
+
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
12610
|
return own;
|
|
12151
12611
|
}
|
|
12152
12612
|
/**
|
|
@@ -12156,11 +12616,11 @@ function ownProduct(channel, own) {
|
|
|
12156
12616
|
* register a server reaching every workspace for a caller who believed they had pinned it.
|
|
12157
12617
|
*/
|
|
12158
12618
|
function checkRequest(request) {
|
|
12159
|
-
if (!
|
|
12619
|
+
if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
12160
12620
|
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
12621
|
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
12622
|
if (request.name !== void 0) checkServerName(request.name);
|
|
12163
|
-
const label =
|
|
12623
|
+
const label = serverLabel(request.channel);
|
|
12164
12624
|
const refuse = (option) => {
|
|
12165
12625
|
const owner = narrowingOwner(option);
|
|
12166
12626
|
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 +12656,7 @@ function checkPin(config, request) {
|
|
|
12196
12656
|
return;
|
|
12197
12657
|
}
|
|
12198
12658
|
const { account } = resolveName(config, "account", pin);
|
|
12199
|
-
if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${
|
|
12659
|
+
if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${serverLabel(request.channel)} ${accountNoun(request.channel)}`);
|
|
12200
12660
|
}
|
|
12201
12661
|
function installOptions(request) {
|
|
12202
12662
|
const own = pinOption(request.channel);
|
|
@@ -12215,7 +12675,7 @@ function installOptions(request) {
|
|
|
12215
12675
|
};
|
|
12216
12676
|
}
|
|
12217
12677
|
/**
|
|
12218
|
-
* Registering one
|
|
12678
|
+
* Registering one declared server with one client, as a change.
|
|
12219
12679
|
*
|
|
12220
12680
|
* The effects are what a person is agreeing to, one sentence each, and everything that decides what the server may
|
|
12221
12681
|
* reach is in them — which server, which client, under which name, pinned to what, replacing what, started how. The
|
|
@@ -12230,21 +12690,22 @@ function installOptions(request) {
|
|
|
12230
12690
|
* resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
|
|
12231
12691
|
* from another build of the checkout — is another registration, refused as one.
|
|
12232
12692
|
*/
|
|
12233
|
-
function serverInstallChange(core, env, request, own) {
|
|
12693
|
+
function serverInstallChange(core, env, request, own, deps = {}) {
|
|
12234
12694
|
checkRequest(request);
|
|
12235
12695
|
ownProduct(request.channel, own);
|
|
12236
12696
|
const context = {
|
|
12237
12697
|
env,
|
|
12238
|
-
core
|
|
12698
|
+
core,
|
|
12699
|
+
...deps.platform === void 0 ? {} : { platform: deps.platform }
|
|
12239
12700
|
};
|
|
12240
|
-
const facts =
|
|
12241
|
-
const label =
|
|
12701
|
+
const facts = serverFacts(request.channel);
|
|
12702
|
+
const label = serverLabel(request.channel);
|
|
12242
12703
|
const noun = accountNoun(request.channel);
|
|
12243
12704
|
const pinName = pinOption(request.channel);
|
|
12244
12705
|
const switchable = hasNarrowing(request.channel, "readOnly");
|
|
12245
12706
|
const name = request.name ?? facts.defaultServerName;
|
|
12246
12707
|
const launcher = request.launcher ?? "managed";
|
|
12247
|
-
const productNow = async () => own ?? await
|
|
12708
|
+
const productNow = async () => own ?? await serverProduct(request.channel, request.launcher);
|
|
12248
12709
|
let planned;
|
|
12249
12710
|
return {
|
|
12250
12711
|
plan: async (config) => {
|
|
@@ -12275,6 +12736,14 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12275
12736
|
else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
|
|
12276
12737
|
}
|
|
12277
12738
|
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)}`);
|
|
12739
|
+
if (request.print !== true && launcher === "managed" && managedShimsSupported(context)) {
|
|
12740
|
+
const path = managedShimPath({
|
|
12741
|
+
env,
|
|
12742
|
+
dataDir: core.paths.dataDir,
|
|
12743
|
+
platform: context.platform
|
|
12744
|
+
}, facts.binary);
|
|
12745
|
+
effects.push(`writes or repoints the owned CLI shim at ${path}; a foreign file there is left unchanged`);
|
|
12746
|
+
}
|
|
12278
12747
|
return {
|
|
12279
12748
|
before: config,
|
|
12280
12749
|
after: config,
|
|
@@ -12284,9 +12753,10 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12284
12753
|
},
|
|
12285
12754
|
apply: async () => {
|
|
12286
12755
|
if (planned === void 0) throw new CommsError("UNEXPECTED", "the registration was applied before it was planned");
|
|
12287
|
-
const
|
|
12756
|
+
const installed = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
|
|
12288
12757
|
throw await remedyLocated(error, core);
|
|
12289
12758
|
});
|
|
12759
|
+
const result = request.print === true ? installed : await withManagedShim(context, planned.product, installed);
|
|
12290
12760
|
return {
|
|
12291
12761
|
...result,
|
|
12292
12762
|
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 +12765,68 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12295
12765
|
};
|
|
12296
12766
|
}
|
|
12297
12767
|
/**
|
|
12768
|
+
* A managed install's CLI shim (CUE-406), once the registration's outcome is known: the command the locator finds in
|
|
12769
|
+
* the runtime the entry starts, written where nothing is or over this installation's own unchanged shim, and never
|
|
12770
|
+
* over anything else. A shim that cannot be written leaves the registration as it went, and says how to repair both.
|
|
12771
|
+
* The installer may not import the locator, so the operation writes it — for `mcp install` and `update` alike.
|
|
12772
|
+
*/
|
|
12773
|
+
async function withManagedShim(context, product, installed) {
|
|
12774
|
+
if (installed.launcher !== "managed") return installed;
|
|
12775
|
+
const cliEntry = installed.entry.args[0];
|
|
12776
|
+
if (cliEntry === void 0) throw new CommsError("UNEXPECTED", "the managed entry has no CLI path");
|
|
12777
|
+
const shimContext = {
|
|
12778
|
+
env: context.env,
|
|
12779
|
+
dataDir: context.core.paths.dataDir,
|
|
12780
|
+
platform: context.platform
|
|
12781
|
+
};
|
|
12782
|
+
if (!managedShimsSupported(shimContext)) return {
|
|
12783
|
+
...installed,
|
|
12784
|
+
shimNotice: managedShimUnsupportedNotice(shimContext)
|
|
12785
|
+
};
|
|
12786
|
+
const warnings = [...installed.warnings];
|
|
12787
|
+
let shim;
|
|
12788
|
+
let shimFailure;
|
|
12789
|
+
try {
|
|
12790
|
+
const located = locateCliCommand({
|
|
12791
|
+
caller: {
|
|
12792
|
+
url: pathToFileURL(cliEntry).href,
|
|
12793
|
+
packageName: product.packageName
|
|
12794
|
+
},
|
|
12795
|
+
target: {
|
|
12796
|
+
packageName: product.packageName,
|
|
12797
|
+
manifest: {
|
|
12798
|
+
label: product.defaultServerName,
|
|
12799
|
+
binary: product.binary
|
|
12800
|
+
}
|
|
12801
|
+
},
|
|
12802
|
+
words: [],
|
|
12803
|
+
uses: [],
|
|
12804
|
+
paths: {
|
|
12805
|
+
...context.core.paths,
|
|
12806
|
+
downloadsDir: context.core.paths.dataDir
|
|
12807
|
+
},
|
|
12808
|
+
platform: context.platform
|
|
12809
|
+
});
|
|
12810
|
+
if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
|
|
12811
|
+
shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
|
|
12812
|
+
binary: product.binary,
|
|
12813
|
+
command: located.command.words,
|
|
12814
|
+
entry: located.command.entry
|
|
12815
|
+
});
|
|
12816
|
+
} catch (error) {
|
|
12817
|
+
shimFailure = error instanceof Error ? error.message : String(error);
|
|
12818
|
+
const path = join(homeDirectory(context.env), ".local", "bin", product.binary);
|
|
12819
|
+
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.`);
|
|
12820
|
+
}
|
|
12821
|
+
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.`);
|
|
12822
|
+
return {
|
|
12823
|
+
...installed,
|
|
12824
|
+
warnings,
|
|
12825
|
+
...shim === void 0 ? {} : { shim },
|
|
12826
|
+
...shimFailure === void 0 ? {} : { shimFailure }
|
|
12827
|
+
};
|
|
12828
|
+
}
|
|
12829
|
+
/**
|
|
12298
12830
|
* A refusal of `mcp install` with its remedy — another install of the same product — said with the command located:
|
|
12299
12831
|
* the printing package's own, core's, or another product's found among this machine's registrations (CUE-403). The
|
|
12300
12832
|
* installer says only the words; it may not import the locator.
|
|
@@ -12302,17 +12834,24 @@ function serverInstallChange(core, env, request, own) {
|
|
|
12302
12834
|
async function remedyLocated(error, core) {
|
|
12303
12835
|
const remedy = installRemedyOf(error);
|
|
12304
12836
|
if (remedy === void 0 || !(error instanceof CommsError)) return error;
|
|
12305
|
-
const
|
|
12306
|
-
if (
|
|
12837
|
+
const server = SERVERS.find((each) => serverFacts(each).binary === remedy.binary);
|
|
12838
|
+
if (server === void 0) return error;
|
|
12307
12839
|
const maker = await requireHandoffs(core).registered();
|
|
12840
|
+
const handoff = isChannel(server) ? maker.of(server, remedy.words) : maker.core([
|
|
12841
|
+
"mcp",
|
|
12842
|
+
"install",
|
|
12843
|
+
"--server",
|
|
12844
|
+
server,
|
|
12845
|
+
...remedy.words.slice(2)
|
|
12846
|
+
]);
|
|
12308
12847
|
return new CommsError(error.code, error.message, {
|
|
12309
|
-
hint: handoffSentence(
|
|
12848
|
+
hint: handoffSentence(handoff, remedy.say),
|
|
12310
12849
|
...error.details === void 0 ? {} : { details: error.details },
|
|
12311
12850
|
...error.cause === void 0 ? {} : { cause: error.cause }
|
|
12312
12851
|
});
|
|
12313
12852
|
}
|
|
12314
12853
|
/**
|
|
12315
|
-
* Removing a
|
|
12854
|
+
* Removing a declared server's unused managed runtimes, as a change.
|
|
12316
12855
|
*
|
|
12317
12856
|
* A dry run is free: it removes nothing. Otherwise the plan is that same dry run, and each runtime it would remove is
|
|
12318
12857
|
* one effect — the removal cannot be taken back — so the approval is bound to exactly that list; and the removal is
|
|
@@ -12322,20 +12861,20 @@ async function remedyLocated(error, core) {
|
|
|
12322
12861
|
* release of the command that was run; checked to be that channel's, as for an install.
|
|
12323
12862
|
*/
|
|
12324
12863
|
function serverPruneChange(core, env, request, own) {
|
|
12325
|
-
if (!
|
|
12864
|
+
if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
|
|
12326
12865
|
const context = {
|
|
12327
12866
|
env,
|
|
12328
12867
|
core
|
|
12329
12868
|
};
|
|
12330
12869
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
12331
|
-
packageName:
|
|
12332
|
-
version: "0.
|
|
12870
|
+
packageName: serverFacts(request.channel).packageName,
|
|
12871
|
+
version: "0.18.0"
|
|
12333
12872
|
};
|
|
12334
12873
|
const product = {
|
|
12335
12874
|
packageName,
|
|
12336
12875
|
version
|
|
12337
12876
|
};
|
|
12338
|
-
const label =
|
|
12877
|
+
const label = serverLabel(request.channel);
|
|
12339
12878
|
const base = {
|
|
12340
12879
|
includePrinted: request.includePrinted === true,
|
|
12341
12880
|
...request.processes ? { processes: request.processes } : {}
|
|
@@ -12356,7 +12895,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
12356
12895
|
return {
|
|
12357
12896
|
before: config,
|
|
12358
12897
|
after: config,
|
|
12359
|
-
effects: preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`),
|
|
12898
|
+
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
12899
|
summary: `Remove ${approved.length} unused ${label} runtime${approved.length === 1 ? "" : "s"}`
|
|
12361
12900
|
};
|
|
12362
12901
|
},
|
|
@@ -12635,21 +13174,22 @@ function updateStopMessage(pending, where, commands) {
|
|
|
12635
13174
|
* the yes is a different change, and the claim is refused. Nothing behind, nothing is prepared.
|
|
12636
13175
|
*/
|
|
12637
13176
|
/**
|
|
12638
|
-
* The packages this suite publishes, whose latest releases an update reads: every
|
|
12639
|
-
* every package a
|
|
13177
|
+
* The packages this suite publishes, whose latest releases an update reads: every declared server owner's, from the
|
|
13178
|
+
* channel and service declarations, and every package a server is run through by `npx` when that is another one —
|
|
13179
|
+
* Gmail's `gmail-mcp`.
|
|
12640
13180
|
*/
|
|
12641
|
-
const
|
|
12642
|
-
const SERVER_PACKAGES = Object.freeze(
|
|
12643
|
-
const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...
|
|
12644
|
-
function narrowingArgs(
|
|
12645
|
-
return
|
|
13181
|
+
const OWNER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).packageName));
|
|
13182
|
+
const SERVER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).npxPackage).filter((name) => !OWNER_PACKAGES.includes(name)));
|
|
13183
|
+
const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...OWNER_PACKAGES, ...SERVER_PACKAGES])].sort());
|
|
13184
|
+
function narrowingArgs(server, narrowing) {
|
|
13185
|
+
return serverFacts(server).serverArgs({
|
|
12646
13186
|
client: "json",
|
|
12647
13187
|
...narrowing
|
|
12648
13188
|
});
|
|
12649
13189
|
}
|
|
12650
|
-
/** Every registration of every
|
|
13190
|
+
/** Every registration of every declared server, as the core's `channels` finds them — nothing an `env` holds. */
|
|
12651
13191
|
function registrations(servers) {
|
|
12652
|
-
return
|
|
13192
|
+
return SERVERS.flatMap((channel) => servers.filter((server) => isProductServer(server, serverFacts(channel))).map((server) => ({
|
|
12653
13193
|
channel,
|
|
12654
13194
|
server
|
|
12655
13195
|
})));
|
|
@@ -12687,7 +13227,7 @@ async function latestReleases(names, latestVersion) {
|
|
|
12687
13227
|
* have no line to paste: the reasons below give it with `handoffSentence`, which shows it as words then.
|
|
12688
13228
|
*/
|
|
12689
13229
|
function installHandoff(item, handoffs) {
|
|
12690
|
-
const facts =
|
|
13230
|
+
const facts = serverFacts(item.channel);
|
|
12691
13231
|
const words = [
|
|
12692
13232
|
"mcp",
|
|
12693
13233
|
"install",
|
|
@@ -12696,7 +13236,13 @@ function installHandoff(item, handoffs) {
|
|
|
12696
13236
|
];
|
|
12697
13237
|
if (item.name !== facts.defaultServerName) words.push("--name", item.name);
|
|
12698
13238
|
words.push(...item.narrowing, "--force");
|
|
12699
|
-
return handoffs.of(item.channel, words)
|
|
13239
|
+
return isChannel(item.channel) ? handoffs.of(item.channel, words) : handoffs.core([
|
|
13240
|
+
"mcp",
|
|
13241
|
+
"install",
|
|
13242
|
+
"--server",
|
|
13243
|
+
item.channel,
|
|
13244
|
+
...words.slice(2)
|
|
13245
|
+
]);
|
|
12700
13246
|
}
|
|
12701
13247
|
/**
|
|
12702
13248
|
* Why a registration that is behind cannot be registered again from here, or undefined when it can.
|
|
@@ -12736,8 +13282,8 @@ function newest(versions) {
|
|
|
12736
13282
|
*/
|
|
12737
13283
|
async function runtimesNeeded(dataDir, latest, managed) {
|
|
12738
13284
|
const needed = [];
|
|
12739
|
-
for (const channel of
|
|
12740
|
-
const packageName =
|
|
13285
|
+
for (const channel of SERVERS) {
|
|
13286
|
+
const packageName = serverFacts(channel).packageName;
|
|
12741
13287
|
const version = latest[packageName];
|
|
12742
13288
|
if (version === void 0) continue;
|
|
12743
13289
|
if (!managed.some((item) => item.channel === channel && item.launcher === "managed")) continue;
|
|
@@ -12764,7 +13310,7 @@ async function inspect(core, env, deps) {
|
|
|
12764
13310
|
const handoffs = requireHandoffs(core, deps.platform).withRegistrations(scan.servers);
|
|
12765
13311
|
const unreadable = [...scan.unreadable];
|
|
12766
13312
|
const found = registrations(scan.servers).map(({ channel, server }) => {
|
|
12767
|
-
const facts =
|
|
13313
|
+
const facts = serverFacts(channel);
|
|
12768
13314
|
return {
|
|
12769
13315
|
channel,
|
|
12770
13316
|
server,
|
|
@@ -12784,9 +13330,9 @@ async function inspect(core, env, deps) {
|
|
|
12784
13330
|
reason: error instanceof Error ? error.message : String(error)
|
|
12785
13331
|
});
|
|
12786
13332
|
}
|
|
12787
|
-
const wanted = /* @__PURE__ */ new Set([
|
|
13333
|
+
const wanted = /* @__PURE__ */ new Set([serverFacts("core").packageName]);
|
|
12788
13334
|
for (const entry of found) {
|
|
12789
|
-
wanted.add(
|
|
13335
|
+
wanted.add(serverFacts(entry.channel).packageName);
|
|
12790
13336
|
wanted.add(entry.package);
|
|
12791
13337
|
}
|
|
12792
13338
|
for (const name of Object.keys(installed)) wanted.add(name);
|
|
@@ -12809,7 +13355,7 @@ async function inspect(core, env, deps) {
|
|
|
12809
13355
|
name: entry.server.name,
|
|
12810
13356
|
scope: entry.server.scope ?? "user",
|
|
12811
13357
|
path: entry.server.path,
|
|
12812
|
-
launcher: launcherOf(entry.server,
|
|
13358
|
+
launcher: launcherOf(entry.server, serverFacts(entry.channel)),
|
|
12813
13359
|
version: entry.version,
|
|
12814
13360
|
latest: latest[entry.package] ?? "",
|
|
12815
13361
|
narrowing: narrowingArgs(entry.channel, entry.narrowing)
|
|
@@ -12840,8 +13386,8 @@ async function inspect(core, env, deps) {
|
|
|
12840
13386
|
}
|
|
12841
13387
|
const needed = await runtimesNeeded(core.paths.dataDir, latest, candidates.map((candidate) => candidate.item));
|
|
12842
13388
|
behind.push(...needed);
|
|
12843
|
-
for (const channel of
|
|
12844
|
-
const packageName =
|
|
13389
|
+
for (const channel of SERVERS) {
|
|
13390
|
+
const packageName = serverFacts(channel).packageName;
|
|
12845
13391
|
const version = latest[packageName];
|
|
12846
13392
|
if (version === void 0 || await reusableRuntime(core.paths.dataDir, packageName, version) === null) continue;
|
|
12847
13393
|
upToDate.push({
|
|
@@ -12899,7 +13445,7 @@ async function updateCheck(core, env, deps = {}, claim) {
|
|
|
12899
13445
|
await recordFound(core, env, report, deps, null, claim);
|
|
12900
13446
|
return report;
|
|
12901
13447
|
}
|
|
12902
|
-
const CORE_PACKAGE$1 =
|
|
13448
|
+
const CORE_PACKAGE$1 = serverFacts("core").packageName;
|
|
12903
13449
|
/**
|
|
12904
13450
|
* One registration: its channel, its client, its scope, the file it is in and its name. Two copies of it in one file —
|
|
12905
13451
|
* `servers` and an old `mcpServers` — are one registration, and an update registers them again as one.
|
|
@@ -12978,7 +13524,7 @@ function updateCheckFindings(report, latest, after = null) {
|
|
|
12978
13524
|
});
|
|
12979
13525
|
const globals = items.filter((item) => item.kind === "global");
|
|
12980
13526
|
const global = CHANNELS.filter((channel) => {
|
|
12981
|
-
const name =
|
|
13527
|
+
const name = serverFacts(channel).packageName;
|
|
12982
13528
|
return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.version));
|
|
12983
13529
|
});
|
|
12984
13530
|
const stillBehind = after === null ? report.behind.length > 0 : !(after.ok && after.manual.length === 0 && after.status !== "manual");
|
|
@@ -13009,13 +13555,11 @@ function reachOf(channel, narrowing) {
|
|
|
13009
13555
|
*/
|
|
13010
13556
|
function registrationEffects(step) {
|
|
13011
13557
|
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];
|
|
13558
|
+
const reach = reachOf(item.channel, serverFacts(item.channel).narrowingOf(item.narrowing));
|
|
13559
|
+
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];
|
|
13560
|
+
if (step.shim) effects.push(`writes or repoints the owned CLI shim at ${step.shim}; a foreign file there is left unchanged`);
|
|
13561
|
+
if (item.launcher === "npx") effects.push(`${item.client} will fetch ${item.package}@${item.latest} from npm each time it starts "${item.name}"`);
|
|
13562
|
+
return effects;
|
|
13019
13563
|
}
|
|
13020
13564
|
/** Every step as the sentences a person reads, in the order the steps are taken. */
|
|
13021
13565
|
function effectsOf(planned) {
|
|
@@ -13051,7 +13595,7 @@ async function planRegistrations(context, candidates, request, handoffs) {
|
|
|
13051
13595
|
if (seen.has(key)) continue;
|
|
13052
13596
|
seen.add(key);
|
|
13053
13597
|
const launcher = item.launcher;
|
|
13054
|
-
const product = await
|
|
13598
|
+
const product = await serverProduct(item.channel, launcher, item.latest);
|
|
13055
13599
|
const options = {
|
|
13056
13600
|
client: item.client,
|
|
13057
13601
|
name: item.name,
|
|
@@ -13077,7 +13621,12 @@ async function planRegistrations(context, candidates, request, handoffs) {
|
|
|
13077
13621
|
product,
|
|
13078
13622
|
options,
|
|
13079
13623
|
install: plannedInstall(preflight),
|
|
13080
|
-
destination: entryDestination(item.client, preflight.target, preflight.command, context.env)
|
|
13624
|
+
destination: entryDestination(item.client, preflight.target, preflight.command, context.env),
|
|
13625
|
+
...launcher === "managed" && managedShimsSupported(context) ? { shim: managedShimPath({
|
|
13626
|
+
env: context.env,
|
|
13627
|
+
dataDir: context.core.paths.dataDir,
|
|
13628
|
+
platform: context.platform
|
|
13629
|
+
}, product.binary) } : {}
|
|
13081
13630
|
});
|
|
13082
13631
|
}
|
|
13083
13632
|
return {
|
|
@@ -13199,18 +13748,23 @@ function whatBecameOf(stored) {
|
|
|
13199
13748
|
revoked: "it had been revoked"
|
|
13200
13749
|
}[record.state] ?? `it is ${JSON.stringify(record.state)}, a state this version does not know`;
|
|
13201
13750
|
}
|
|
13202
|
-
/** The product a runtime of `packageName` is installed as: the
|
|
13751
|
+
/** The product a runtime of `packageName` is installed as: the declared server whose package it is. */
|
|
13203
13752
|
function runtimeProduct(packageName, version) {
|
|
13204
|
-
const channel =
|
|
13205
|
-
if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a
|
|
13206
|
-
return
|
|
13753
|
+
const channel = SERVERS.find((each) => serverFacts(each).packageName === packageName);
|
|
13754
|
+
if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a declared server package`);
|
|
13755
|
+
return serverProduct(channel, "managed", version);
|
|
13207
13756
|
}
|
|
13208
13757
|
/**
|
|
13209
|
-
* Every
|
|
13210
|
-
*
|
|
13758
|
+
* Every declared server's `mcp prune`, as a person runs it at a terminal: each channel command is located among this
|
|
13759
|
+
* machine's registrations, and each service uses core — never a bare name (CUE-403).
|
|
13211
13760
|
*/
|
|
13212
13761
|
function pruneCommands(handoffs) {
|
|
13213
|
-
return handoffChoices(
|
|
13762
|
+
return handoffChoices(SERVERS.map((channel) => isChannel(channel) ? handoffs.of(channel, ["mcp", "prune"]) : handoffs.core([
|
|
13763
|
+
"mcp",
|
|
13764
|
+
"prune",
|
|
13765
|
+
"--server",
|
|
13766
|
+
channel
|
|
13767
|
+
])), "none of their commands is locatable here:", { conjunction: "and" });
|
|
13214
13768
|
}
|
|
13215
13769
|
function message(error) {
|
|
13216
13770
|
return error instanceof CommsError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -13267,26 +13821,28 @@ async function applyUpdate(context, planned, deps) {
|
|
|
13267
13821
|
continue;
|
|
13268
13822
|
}
|
|
13269
13823
|
try {
|
|
13270
|
-
const pins =
|
|
13271
|
-
const
|
|
13824
|
+
const pins = serverFacts(item.channel).narrowingOf(item.narrowing);
|
|
13825
|
+
const installed = await mcpInstall(context, product, {
|
|
13272
13826
|
...options,
|
|
13273
13827
|
...pins
|
|
13274
13828
|
}, install);
|
|
13275
|
-
if (!
|
|
13829
|
+
if (!installed.applied) {
|
|
13276
13830
|
steps.push({
|
|
13277
13831
|
...base,
|
|
13278
13832
|
outcome: "failed",
|
|
13279
|
-
detail:
|
|
13833
|
+
detail: installed.notApplied ?? "nothing was registered"
|
|
13280
13834
|
});
|
|
13281
13835
|
continue;
|
|
13282
13836
|
}
|
|
13837
|
+
const result = await withManagedShim(context, product, installed);
|
|
13283
13838
|
steps.push({
|
|
13284
13839
|
...base,
|
|
13285
13840
|
outcome: "registered",
|
|
13286
13841
|
verification: result.verification,
|
|
13287
13842
|
...result.verifyDetail !== void 0 ? { detail: result.verifyDetail } : {},
|
|
13288
13843
|
...result.backupPath !== void 0 ? { backupPath: result.backupPath } : {},
|
|
13289
|
-
...result.warnings.length > 0 ? { warnings: result.warnings } : {}
|
|
13844
|
+
...result.warnings.length > 0 ? { warnings: result.warnings } : {},
|
|
13845
|
+
...result.shim !== void 0 ? { shim: result.shim } : {}
|
|
13290
13846
|
});
|
|
13291
13847
|
} catch (error) {
|
|
13292
13848
|
steps.push({
|
|
@@ -14046,7 +14602,7 @@ function terminalUpdateHooks(core, env, options) {
|
|
|
14046
14602
|
};
|
|
14047
14603
|
}
|
|
14048
14604
|
//#endregion
|
|
14049
|
-
//#region ../core/dist/tool-arguments-
|
|
14605
|
+
//#region ../core/dist/tool-arguments-C_RA7n3A.mjs
|
|
14050
14606
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
14051
14607
|
const TARGET = "draft-2020-12";
|
|
14052
14608
|
/**
|
|
@@ -14351,7 +14907,7 @@ function spoken(words, joiner) {
|
|
|
14351
14907
|
return `${words.slice(0, -1).join(", ")} ${joiner} ${words.at(-1)}`;
|
|
14352
14908
|
}
|
|
14353
14909
|
//#endregion
|
|
14354
|
-
//#region ../../node_modules/.pnpm/postal-mime@
|
|
14910
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/decode-strings.js
|
|
14355
14911
|
const textEncoder = new TextEncoder();
|
|
14356
14912
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
14357
14913
|
const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
|
|
@@ -14406,26 +14962,143 @@ const codePagePattern = /^(?:cp|windows|ms|ibm)(\d+)$/;
|
|
|
14406
14962
|
function normalizeCharset(charset) {
|
|
14407
14963
|
return charset.replace(/^(?:x-ms-|x-|cs)/, "").replace(/[\s._-]+/g, "");
|
|
14408
14964
|
}
|
|
14965
|
+
const ENCODING_LABELS = new Set([
|
|
14966
|
+
"unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8",
|
|
14967
|
+
"866 cp866 csibm866 ibm866",
|
|
14968
|
+
"csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2",
|
|
14969
|
+
"csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3",
|
|
14970
|
+
"csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4",
|
|
14971
|
+
"csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988",
|
|
14972
|
+
"arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e",
|
|
14973
|
+
"iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987",
|
|
14974
|
+
"csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7",
|
|
14975
|
+
"iso_8859-7:1987 sun_eu_greek",
|
|
14976
|
+
"csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8",
|
|
14977
|
+
"iso_8859-8:1988 visual",
|
|
14978
|
+
"csiso88598i iso-8859-8-i logical",
|
|
14979
|
+
"csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6",
|
|
14980
|
+
"iso-8859-13 iso8859-13 iso885913",
|
|
14981
|
+
"iso-8859-14 iso8859-14 iso885914",
|
|
14982
|
+
"csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9",
|
|
14983
|
+
"iso-8859-16",
|
|
14984
|
+
"cskoi8r koi koi8 koi8-r koi8_r",
|
|
14985
|
+
"koi8-ru koi8-u",
|
|
14986
|
+
"csmacintosh mac macintosh x-mac-roman",
|
|
14987
|
+
"dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874",
|
|
14988
|
+
"cp1250 windows-1250 x-cp1250",
|
|
14989
|
+
"cp1251 windows-1251 x-cp1251",
|
|
14990
|
+
"ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591",
|
|
14991
|
+
"iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252",
|
|
14992
|
+
"cp1253 windows-1253 x-cp1253",
|
|
14993
|
+
"cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5",
|
|
14994
|
+
"windows-1254 x-cp1254",
|
|
14995
|
+
"cp1255 windows-1255 x-cp1255",
|
|
14996
|
+
"cp1256 windows-1256 x-cp1256",
|
|
14997
|
+
"cp1257 windows-1257 x-cp1257",
|
|
14998
|
+
"cp1258 windows-1258 x-cp1258",
|
|
14999
|
+
"x-mac-cyrillic x-mac-ukrainian",
|
|
15000
|
+
"chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk",
|
|
15001
|
+
"gb18030",
|
|
15002
|
+
"big5 big5-hkscs cn-big5 csbig5 x-x-big5",
|
|
15003
|
+
"cseucpkdfmtjapanese euc-jp x-euc-jp",
|
|
15004
|
+
"csiso2022jp iso-2022-jp",
|
|
15005
|
+
"csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis",
|
|
15006
|
+
"cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
|
|
15007
|
+
"windows-949",
|
|
15008
|
+
"csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
|
|
15009
|
+
"unicodefffe utf-16be",
|
|
15010
|
+
"csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
|
|
15011
|
+
"x-user-defined"
|
|
15012
|
+
].join(" ").split(" "));
|
|
15013
|
+
const decoders = /* @__PURE__ */ new Map();
|
|
14409
15014
|
function tryDecoder(charset) {
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
15015
|
+
if (!ENCODING_LABELS.has(charset)) return null;
|
|
15016
|
+
let decoder = decoders.get(charset);
|
|
15017
|
+
if (decoder === void 0) {
|
|
15018
|
+
decoder = null;
|
|
15019
|
+
try {
|
|
15020
|
+
decoder = new TextDecoder(charset);
|
|
15021
|
+
} catch {}
|
|
15022
|
+
decoders.set(charset, decoder);
|
|
14414
15023
|
}
|
|
15024
|
+
return decoder;
|
|
14415
15025
|
}
|
|
14416
|
-
|
|
15026
|
+
const utf8Decoder = new TextDecoder();
|
|
15027
|
+
/**
|
|
15028
|
+
* Finds the decoder for a charset label, or null when the runtime has no encoding for it
|
|
15029
|
+
*/
|
|
15030
|
+
function findDecoder(charset) {
|
|
14417
15031
|
charset = (charset || "utf8").trim().toLowerCase();
|
|
14418
15032
|
const decoder = tryDecoder(charset);
|
|
14419
15033
|
if (decoder) return decoder;
|
|
14420
15034
|
const normalized = normalizeCharset(charset);
|
|
14421
15035
|
const codePage = normalized.match(codePagePattern);
|
|
14422
|
-
return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized)
|
|
15036
|
+
return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized);
|
|
15037
|
+
}
|
|
15038
|
+
function getDecoder(charset) {
|
|
15039
|
+
return findDecoder(charset) || tryDecoder("windows-1252") || utf8Decoder;
|
|
15040
|
+
}
|
|
15041
|
+
/**
|
|
15042
|
+
* Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
|
|
15043
|
+
* the exception one throws costs more than the decode itself and a message decides how
|
|
15044
|
+
* many lines throw it.
|
|
15045
|
+
*/
|
|
15046
|
+
function isUtf8(bytes) {
|
|
15047
|
+
for (let i = 0; i < bytes.length;) {
|
|
15048
|
+
const lead = bytes[i];
|
|
15049
|
+
if (lead < 128) {
|
|
15050
|
+
i++;
|
|
15051
|
+
continue;
|
|
15052
|
+
}
|
|
15053
|
+
let count;
|
|
15054
|
+
if (lead >= 194 && lead <= 223) count = 1;
|
|
15055
|
+
else if (lead >= 224 && lead <= 239) count = 2;
|
|
15056
|
+
else if (lead >= 240 && lead <= 244) count = 3;
|
|
15057
|
+
else return false;
|
|
15058
|
+
if (i + count >= bytes.length) return false;
|
|
15059
|
+
const second = bytes[i + 1];
|
|
15060
|
+
if (second < (lead === 224 ? 160 : lead === 240 ? 144 : 128) || second > (lead === 237 ? 159 : lead === 244 ? 143 : 191)) return false;
|
|
15061
|
+
for (let k = 2; k <= count; k++) if ((bytes[i + k] & 192) !== 128) return false;
|
|
15062
|
+
i += count + 1;
|
|
15063
|
+
}
|
|
15064
|
+
return true;
|
|
15065
|
+
}
|
|
15066
|
+
/**
|
|
15067
|
+
* Decodes bytes whose encoding is not known: as UTF-8 when they are well formed UTF-8, as
|
|
15068
|
+
* windows-1252 otherwise.
|
|
15069
|
+
*
|
|
15070
|
+
* A message that declares no charset, or one the runtime has never heard of, usually
|
|
15071
|
+
* predates RFC 6532 or comes from a generator that never heard of it either, and its 8-bit
|
|
15072
|
+
* bytes are a legacy single byte encoding far more often than broken UTF-8. windows-1252
|
|
15073
|
+
* is the fallback the WHATWG Encoding Standard prescribes and what a mail client's default
|
|
15074
|
+
* charset amounts to, and it reads such text rather than filling it with replacement
|
|
15075
|
+
* characters.
|
|
15076
|
+
*
|
|
15077
|
+
* @param bytes Bytes to decode
|
|
15078
|
+
* @param utf8 The UTF-8 decoder to use, header lines bring one that keeps a BOM
|
|
15079
|
+
* @return Decoded text
|
|
15080
|
+
*/
|
|
15081
|
+
function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
|
|
15082
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
15083
|
+
return isUtf8(view) ? utf8.decode(view) : getDecoder("windows-1252").decode(view);
|
|
15084
|
+
}
|
|
15085
|
+
/**
|
|
15086
|
+
* Decodes bytes in a named charset. A label the runtime does not know is no charset at
|
|
15087
|
+
* all, see decodeUtf8OrWindows1252.
|
|
15088
|
+
*
|
|
15089
|
+
* @param bytes Bytes to decode
|
|
15090
|
+
* @param charset Charset label
|
|
15091
|
+
* @return Decoded text
|
|
15092
|
+
*/
|
|
15093
|
+
function decodeCharset(bytes, charset) {
|
|
15094
|
+
const decoder = findDecoder(charset);
|
|
15095
|
+
return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
|
|
14423
15096
|
}
|
|
14424
15097
|
/**
|
|
14425
15098
|
* Numeric value of an ASCII hex digit
|
|
14426
15099
|
*
|
|
14427
|
-
* @param
|
|
14428
|
-
* @return
|
|
15100
|
+
* @param c Byte to read
|
|
15101
|
+
* @return Value 0-15, or -1 if the byte is not a hex digit
|
|
14429
15102
|
*/
|
|
14430
15103
|
function hexNibble(c) {
|
|
14431
15104
|
if (c >= 48 && c <= 57) return c - 48;
|
|
@@ -14434,14 +15107,35 @@ function hexNibble(c) {
|
|
|
14434
15107
|
return -1;
|
|
14435
15108
|
}
|
|
14436
15109
|
/**
|
|
15110
|
+
* Folds the line breaks of a header value into spaces.
|
|
15111
|
+
*
|
|
15112
|
+
* A header value holds no line breaks. The raw value is folded when the headers are
|
|
15113
|
+
* processed, see processHeaders in mime-node.ts, an RFC 2047 encoded word is folded as it
|
|
15114
|
+
* is decoded, and an RFC 2231 filename is folded where the attachment is built. Otherwise a
|
|
15115
|
+
* decoded name, subject or filename written into a header of an outgoing message by the
|
|
15116
|
+
* consumer ends the header early and starts another one with the rest of the value.
|
|
15117
|
+
*
|
|
15118
|
+
* The RFC 2231 percent decoder itself does not fold: it also produces the boundary, and a
|
|
15119
|
+
* boundary holding a line break can never match a line. That has to stay so, or a message
|
|
15120
|
+
* that has no parts for every other parser would have them here.
|
|
15121
|
+
*
|
|
15122
|
+
* @param str Header value
|
|
15123
|
+
* @return The value with every run of CR and LF replaced by a space
|
|
15124
|
+
*/
|
|
15125
|
+
function foldLineBreaks(str) {
|
|
15126
|
+
return str.replace(/[\r\n]+/g, " ");
|
|
15127
|
+
}
|
|
15128
|
+
/**
|
|
14437
15129
|
* Decode a complete mime word encoded string
|
|
14438
15130
|
*
|
|
14439
|
-
* @param
|
|
14440
|
-
* @
|
|
15131
|
+
* @param charset Character set of the encoded word
|
|
15132
|
+
* @param encoding `Q` or `B`
|
|
15133
|
+
* @param str Mime word encoded string
|
|
15134
|
+
* @return Decoded unicode string
|
|
14441
15135
|
*/
|
|
14442
15136
|
function decodeWord(charset, encoding, str) {
|
|
14443
15137
|
let splitPos = charset.indexOf("*");
|
|
14444
|
-
if (splitPos >= 0) charset = charset.
|
|
15138
|
+
if (splitPos >= 0) charset = charset.slice(0, splitPos);
|
|
14445
15139
|
encoding = encoding.toUpperCase();
|
|
14446
15140
|
let byteStr;
|
|
14447
15141
|
if (encoding === "Q") {
|
|
@@ -14466,7 +15160,7 @@ function decodeWord(charset, encoding, str) {
|
|
|
14466
15160
|
for (let i = 0, len = encodedBytes.length; i < len; i++) dataView.setUint8(i, encodedBytes[i]);
|
|
14467
15161
|
} else if (encoding === "B") byteStr = decodeBase64$1(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ""));
|
|
14468
15162
|
else byteStr = textEncoder.encode(str);
|
|
14469
|
-
return
|
|
15163
|
+
return foldLineBreaks(decodeCharset(byteStr, charset));
|
|
14470
15164
|
}
|
|
14471
15165
|
const ENCODED_WORD_PATTERN = "=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=";
|
|
14472
15166
|
const ENCODED_WORD_REGEX = new RegExp(ENCODED_WORD_PATTERN, "g");
|
|
@@ -14476,8 +15170,8 @@ const ENCODED_WORDS_ONLY_REGEX = new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`
|
|
|
14476
15170
|
* Checks whether a string is nothing but RFC 2047 encoded words. Kept next to the grammar
|
|
14477
15171
|
* it depends on, so the pattern has a single definition.
|
|
14478
15172
|
*
|
|
14479
|
-
* @param
|
|
14480
|
-
* @return
|
|
15173
|
+
* @param str String to check
|
|
15174
|
+
* @return true if the string holds encoded words and nothing else
|
|
14481
15175
|
*/
|
|
14482
15176
|
function isEncodedWordsOnly(str) {
|
|
14483
15177
|
return ENCODED_WORDS_ONLY_REGEX.test(str);
|
|
@@ -14489,8 +15183,8 @@ function isEncodedWordsOnly(str) {
|
|
|
14489
15183
|
* means the input can not contain the marker, which previously let a sender delete text
|
|
14490
15184
|
* from a subject or a display name by writing the marker into the header themselves.
|
|
14491
15185
|
*
|
|
14492
|
-
* @param
|
|
14493
|
-
* @return
|
|
15186
|
+
* @param str String to split
|
|
15187
|
+
* @return Array of `{text}` and `{charset, encoding, encodedText}` tokens
|
|
14494
15188
|
*/
|
|
14495
15189
|
function splitEncodedWords(str) {
|
|
14496
15190
|
const tokens = [];
|
|
@@ -14524,9 +15218,9 @@ function canJoinWords(left, right) {
|
|
|
14524
15218
|
/**
|
|
14525
15219
|
* Decodes a token list into a string, optionally merging adjacent encoded words.
|
|
14526
15220
|
*
|
|
14527
|
-
* @param
|
|
14528
|
-
* @param
|
|
14529
|
-
* @return
|
|
15221
|
+
* @param tokens Tokens from splitEncodedWords
|
|
15222
|
+
* @param joinWords Whether adjacent encoded words may be decoded as one unit
|
|
15223
|
+
* @return Decoded string
|
|
14530
15224
|
*/
|
|
14531
15225
|
function renderTokens(tokens, joinWords) {
|
|
14532
15226
|
let result = "";
|
|
@@ -14557,6 +15251,12 @@ function renderTokens(tokens, joinWords) {
|
|
|
14557
15251
|
if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
14558
15252
|
return result;
|
|
14559
15253
|
}
|
|
15254
|
+
/**
|
|
15255
|
+
* Decodes RFC 2047 encoded words in a string
|
|
15256
|
+
*
|
|
15257
|
+
* @param str String that may contain encoded words
|
|
15258
|
+
* @return Unicode string with every encoded word decoded
|
|
15259
|
+
*/
|
|
14560
15260
|
function decodeWords(str) {
|
|
14561
15261
|
const tokens = splitEncodedWords((str || "").toString());
|
|
14562
15262
|
const result = renderTokens(tokens, true);
|
|
@@ -14564,55 +15264,279 @@ function decodeWords(str) {
|
|
|
14564
15264
|
}
|
|
14565
15265
|
new TextDecoder("utf-8", { ignoreBOM: true });
|
|
14566
15266
|
//#endregion
|
|
14567
|
-
//#region ../../node_modules/.pnpm/postal-mime@
|
|
14568
|
-
|
|
14569
|
-
|
|
15267
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/address-parser.js
|
|
15268
|
+
/**
|
|
15269
|
+
* Any whitespace, the test for an angle-addr that needs recovering
|
|
15270
|
+
*/
|
|
15271
|
+
const HAS_WHITESPACE = /\s/;
|
|
14570
15272
|
/**
|
|
14571
|
-
*
|
|
15273
|
+
* An addr-spec that carries its whitespace legally, inside a quoted local part. The
|
|
15274
|
+
* optional tail is the malformed shape: a real mailbox with wreckage trailing it.
|
|
15275
|
+
*/
|
|
15276
|
+
const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
15277
|
+
/**
|
|
15278
|
+
* One run holding a single '@' and no whitespace, the shape an addr-spec has to have
|
|
15279
|
+
*/
|
|
15280
|
+
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
15281
|
+
/**
|
|
15282
|
+
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
15283
|
+
* further '@' that a domain should not have but malformed headers carry anyway
|
|
15284
|
+
*/
|
|
15285
|
+
const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
15286
|
+
/**
|
|
15287
|
+
* An addr-spec sitting inside free text, together with the whitespace around it. Sticky
|
|
15288
|
+
* on purpose: it is only run at the offset looseAddressStart picks, see there.
|
|
15289
|
+
*/
|
|
15290
|
+
const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
15291
|
+
/**
|
|
15292
|
+
* Restores the quoting of a local part that was read out of a quoted string.
|
|
14572
15293
|
*
|
|
14573
|
-
*
|
|
14574
|
-
*
|
|
14575
|
-
*
|
|
14576
|
-
*
|
|
15294
|
+
* RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
|
|
15295
|
+
* on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
|
|
15296
|
+
* domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
|
|
15297
|
+
* same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
|
|
15298
|
+
* recipient separator once the consumer puts the address back into a header.
|
|
15299
|
+
*
|
|
15300
|
+
* @param address Address with an unquoted local part
|
|
15301
|
+
* @return Address with the local part as a quoted-string
|
|
15302
|
+
*/
|
|
15303
|
+
function quoteLocalPart(address) {
|
|
15304
|
+
const lastAt = address.lastIndexOf("@");
|
|
15305
|
+
if (lastAt < 0) return address;
|
|
15306
|
+
const user = address.substring(0, lastAt);
|
|
15307
|
+
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) return address;
|
|
15308
|
+
return "\"" + user.replace(/["\\]/g, "\\$&") + "\"@" + address.substring(lastAt + 1);
|
|
15309
|
+
}
|
|
15310
|
+
/**
|
|
15311
|
+
* The characters JS `\s` matches, which the scan below has to agree with to land on the
|
|
15312
|
+
* same match LOOSE_TEXT_ADDR would.
|
|
15313
|
+
*/
|
|
15314
|
+
function isSpaceCode(code) {
|
|
15315
|
+
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;
|
|
15316
|
+
}
|
|
15317
|
+
/**
|
|
15318
|
+
* The characters JS `\w` matches without the unicode flag, the set the `\b` in
|
|
15319
|
+
* LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
|
|
15320
|
+
* which compares false throughout, so out of range reads as a non-word character.
|
|
15321
|
+
*/
|
|
15322
|
+
function isWordCode(code) {
|
|
15323
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
|
|
15324
|
+
}
|
|
15325
|
+
/**
|
|
15326
|
+
* Whether `\b` holds at an offset
|
|
15327
|
+
*/
|
|
15328
|
+
function isBoundary(text, at) {
|
|
15329
|
+
return isWordCode(text.charCodeAt(at - 1)) !== isWordCode(text.charCodeAt(at));
|
|
15330
|
+
}
|
|
15331
|
+
/**
|
|
15332
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
15333
|
+
* Bounded rather than indexOf, which would scan on to the end of the header once for every
|
|
15334
|
+
* whitespace delimited run and make the walk below quadratic.
|
|
15335
|
+
*/
|
|
15336
|
+
function indexOfAt(text, from, to) {
|
|
15337
|
+
for (let i = from; i < to; i++) if (text.charCodeAt(i) === 64) return i;
|
|
15338
|
+
return -1;
|
|
15339
|
+
}
|
|
15340
|
+
/**
|
|
15341
|
+
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
14577
15342
|
*
|
|
14578
|
-
*
|
|
14579
|
-
*
|
|
14580
|
-
*
|
|
15343
|
+
* Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
|
|
15344
|
+
* offset and rescans the run to the next '@' each time, so a single header well inside
|
|
15345
|
+
* the default size limit could hold a core busy for minutes. The search is also
|
|
15346
|
+
* unnecessary. '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin
|
|
15347
|
+
* at the head of a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b'
|
|
15348
|
+
* gives characters back until it lands on a boundary, so the only end it can take in that
|
|
15349
|
+
* run is the last boundary in it. Both are found in one pass, and the pattern is then run
|
|
15350
|
+
* at that single offset.
|
|
14581
15351
|
*
|
|
14582
|
-
* @param
|
|
14583
|
-
* @return
|
|
15352
|
+
* @param text Free text to look in
|
|
15353
|
+
* @return Offset to match at, or -1
|
|
14584
15354
|
*/
|
|
14585
|
-
function
|
|
14586
|
-
|
|
14587
|
-
let
|
|
14588
|
-
while (
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
let
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
15355
|
+
function looseAddressStart(text) {
|
|
15356
|
+
const len = text.length;
|
|
15357
|
+
let pos = 0;
|
|
15358
|
+
while (pos < len) {
|
|
15359
|
+
while (pos < len && isSpaceCode(text.charCodeAt(pos))) pos++;
|
|
15360
|
+
if (pos >= len) break;
|
|
15361
|
+
const runStart = pos;
|
|
15362
|
+
let runEnd = pos;
|
|
15363
|
+
while (runEnd < len && !isSpaceCode(text.charCodeAt(runEnd))) runEnd++;
|
|
15364
|
+
let at = indexOfAt(text, runStart, runEnd);
|
|
15365
|
+
if (at >= 0) {
|
|
15366
|
+
let lastBoundary = -1;
|
|
15367
|
+
for (let k = runEnd; k > runStart; k--) if (isBoundary(text, k)) {
|
|
15368
|
+
lastBoundary = k;
|
|
15369
|
+
break;
|
|
15370
|
+
}
|
|
15371
|
+
let atomStart = runStart;
|
|
15372
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
15373
|
+
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
15374
|
+
for (let start = atomStart; start < at; start++) if (isBoundary(text, start)) {
|
|
15375
|
+
if (start > runStart) return start;
|
|
15376
|
+
let padded = runStart;
|
|
15377
|
+
while (padded > 0 && isSpaceCode(text.charCodeAt(padded - 1))) padded--;
|
|
15378
|
+
return padded;
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
atomStart = at + 1;
|
|
15382
|
+
at = indexOfAt(text, atomStart, runEnd);
|
|
15383
|
+
}
|
|
15384
|
+
}
|
|
15385
|
+
pos = runEnd;
|
|
15386
|
+
}
|
|
15387
|
+
return -1;
|
|
15388
|
+
}
|
|
15389
|
+
/**
|
|
15390
|
+
* Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
|
|
15391
|
+
*
|
|
15392
|
+
* A malformed header can put more than a mailbox between the angle brackets, most often
|
|
15393
|
+
* because the generator wrote the recipient twice: '<user@example.com user@example.com>'
|
|
15394
|
+
* or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
|
|
15395
|
+
* never be a mailbox, and passing it on as the address loses the recipient that is
|
|
15396
|
+
* sitting right there in the header.
|
|
15397
|
+
*
|
|
15398
|
+
* The run that still reads as an addr-spec is kept and whatever is left over becomes
|
|
15399
|
+
* display text. Candidates are read strictly first and then under the looser grammar,
|
|
15400
|
+
* the same two tiers the unquoted text fallback applies, so that '<a@b@c.com junk>' and a
|
|
15401
|
+
* bare 'a@b@c.com junk' agree on the recipient.
|
|
15402
|
+
*
|
|
15403
|
+
* A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
|
|
15404
|
+
* '<"user name"@example.com>' is well formed and means exactly what it says.
|
|
15405
|
+
*
|
|
15406
|
+
* @param address Collected address
|
|
15407
|
+
* @param text Collected display text
|
|
15408
|
+
* @return The address and the display text, with any leftovers moved to the text
|
|
15409
|
+
*/
|
|
15410
|
+
function recoverAddrSpec(address, text) {
|
|
15411
|
+
if (!HAS_WHITESPACE.test(address)) return {
|
|
15412
|
+
address,
|
|
15413
|
+
text
|
|
15414
|
+
};
|
|
15415
|
+
let rest;
|
|
15416
|
+
const quoted = address.match(QUOTED_LOCAL_ADDR);
|
|
15417
|
+
if (quoted) {
|
|
15418
|
+
if (!quoted[2]) return {
|
|
15419
|
+
address,
|
|
15420
|
+
text
|
|
15421
|
+
};
|
|
15422
|
+
address = quoted[1];
|
|
15423
|
+
rest = [quoted[2]];
|
|
15424
|
+
} else {
|
|
15425
|
+
if (address.indexOf("\"") >= 0) return {
|
|
15426
|
+
address,
|
|
15427
|
+
text
|
|
15428
|
+
};
|
|
15429
|
+
const parts = address.split(/\s+/);
|
|
15430
|
+
let addrIndex = parts.findIndex((part) => ADDR_SPEC.test(part));
|
|
15431
|
+
if (addrIndex < 0) addrIndex = parts.findIndex((part) => LOOSE_ADDR_SPEC.test(part));
|
|
15432
|
+
if (addrIndex < 0) return {
|
|
15433
|
+
address,
|
|
15434
|
+
text
|
|
14601
15435
|
};
|
|
15436
|
+
address = parts.splice(addrIndex, 1)[0];
|
|
15437
|
+
rest = parts;
|
|
14602
15438
|
}
|
|
14603
|
-
return
|
|
15439
|
+
return {
|
|
15440
|
+
address,
|
|
15441
|
+
text: [text, ...rest].filter((part) => part).join(" ")
|
|
15442
|
+
};
|
|
15443
|
+
}
|
|
15444
|
+
/**
|
|
15445
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
15446
|
+
*
|
|
15447
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
15448
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
15449
|
+
* put 'user@example.com(x)evil.com' into the address, a value that is no mailbox and that a
|
|
15450
|
+
* consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
|
|
15451
|
+
* whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
|
|
15452
|
+
* the same rule the token walk applies outside the brackets. Quoted strings and domain
|
|
15453
|
+
* literals are copied through untouched, a '(' in there is not a comment.
|
|
15454
|
+
*
|
|
15455
|
+
* @param address Contents of the angle brackets
|
|
15456
|
+
* @return The address with the comments removed, and the text of the comments
|
|
15457
|
+
*/
|
|
15458
|
+
function stripAddressComments(address) {
|
|
15459
|
+
const comments = [];
|
|
15460
|
+
let result = "";
|
|
15461
|
+
let comment = "";
|
|
15462
|
+
let depth = 0;
|
|
15463
|
+
let closer = "";
|
|
15464
|
+
let lastChar = "";
|
|
15465
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
15466
|
+
const chr = address.charAt(i);
|
|
15467
|
+
if (depth) {
|
|
15468
|
+
if (chr === "\\" && i < len - 1) comment += address.charAt(++i);
|
|
15469
|
+
else if (chr === "(") {
|
|
15470
|
+
depth++;
|
|
15471
|
+
comment += chr;
|
|
15472
|
+
} else if (chr === ")" && !--depth) {
|
|
15473
|
+
comments.push(comment.trim());
|
|
15474
|
+
comment = "";
|
|
15475
|
+
if (lastChar !== "@" && address.charAt(i + 1) !== "@") {
|
|
15476
|
+
result += " ";
|
|
15477
|
+
lastChar = " ";
|
|
15478
|
+
}
|
|
15479
|
+
} else comment += chr;
|
|
15480
|
+
continue;
|
|
15481
|
+
}
|
|
15482
|
+
if (closer) {
|
|
15483
|
+
if (chr === "\\" && closer === "\"" && i < len - 1) {
|
|
15484
|
+
result += chr + address.charAt(++i);
|
|
15485
|
+
lastChar = address.charAt(i);
|
|
15486
|
+
continue;
|
|
15487
|
+
}
|
|
15488
|
+
if (chr === closer) closer = "";
|
|
15489
|
+
} else if (chr === "\"") closer = "\"";
|
|
15490
|
+
else if (chr === "[") closer = "]";
|
|
15491
|
+
else if (chr === "(") {
|
|
15492
|
+
depth = 1;
|
|
15493
|
+
continue;
|
|
15494
|
+
}
|
|
15495
|
+
result += chr;
|
|
15496
|
+
lastChar = chr;
|
|
15497
|
+
}
|
|
15498
|
+
if (depth) comments.push(comment.trim());
|
|
15499
|
+
return {
|
|
15500
|
+
address: result.trim(),
|
|
15501
|
+
comments: comments.filter((text) => text)
|
|
15502
|
+
};
|
|
15503
|
+
}
|
|
15504
|
+
/**
|
|
15505
|
+
* Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
|
|
15506
|
+
* without other angle brackets that has an '@' with at least one character on each side.
|
|
15507
|
+
*
|
|
15508
|
+
* Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
|
|
15509
|
+
* followed by a long run of '@' and no closing `>`.
|
|
15510
|
+
*
|
|
15511
|
+
* @param text Text to check
|
|
15512
|
+
* @return true if an angle bracket address is present
|
|
15513
|
+
*/
|
|
15514
|
+
function hasAngleAddress(text) {
|
|
15515
|
+
let start = -1;
|
|
15516
|
+
let at = -1;
|
|
15517
|
+
for (let i = 0; i < text.length; i++) {
|
|
15518
|
+
const chr = text.charAt(i);
|
|
15519
|
+
if (chr === "<") {
|
|
15520
|
+
start = i + 1;
|
|
15521
|
+
at = -1;
|
|
15522
|
+
} else if (chr === ">") {
|
|
15523
|
+
if (at >= 0 && at < i - 1) return true;
|
|
15524
|
+
start = -1;
|
|
15525
|
+
at = -1;
|
|
15526
|
+
} else if (chr === "@" && start >= 0 && at < 0 && i > start) at = i;
|
|
15527
|
+
}
|
|
15528
|
+
return false;
|
|
14604
15529
|
}
|
|
14605
15530
|
/**
|
|
14606
15531
|
* Converts tokens for a single address into an address object
|
|
14607
15532
|
*
|
|
14608
|
-
* @param
|
|
14609
|
-
* @param
|
|
14610
|
-
* @return
|
|
15533
|
+
* @param tokens Tokens object
|
|
15534
|
+
* @param depth Current recursion depth for nested group protection
|
|
15535
|
+
* @return Address objects
|
|
14611
15536
|
*/
|
|
14612
15537
|
function _handleAddress(tokens, depth) {
|
|
14613
15538
|
let isGroup = false;
|
|
14614
15539
|
let state = "text";
|
|
14615
|
-
let address;
|
|
14616
15540
|
let addresses = [];
|
|
14617
15541
|
let data = {
|
|
14618
15542
|
address: [],
|
|
@@ -14624,6 +15548,12 @@ function _handleAddress(tokens, depth) {
|
|
|
14624
15548
|
let i;
|
|
14625
15549
|
let len;
|
|
14626
15550
|
let insideQuotes = false;
|
|
15551
|
+
const lastChars = {
|
|
15552
|
+
address: "",
|
|
15553
|
+
comment: "",
|
|
15554
|
+
group: "",
|
|
15555
|
+
text: ""
|
|
15556
|
+
};
|
|
14627
15557
|
for (i = 0, len = tokens.length; i < len; i++) {
|
|
14628
15558
|
let token = tokens[i];
|
|
14629
15559
|
let prevToken = i ? tokens[i - 1] : null;
|
|
@@ -14650,14 +15580,17 @@ function _handleAddress(tokens, depth) {
|
|
|
14650
15580
|
insideQuotes = false;
|
|
14651
15581
|
}
|
|
14652
15582
|
else if (token.value) {
|
|
15583
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
15584
|
+
const opensAfterEmptyQuotedString = prevToken?.type === "operator" && prevToken.value === "\"" && !!prevToken.noBreak && prevPrevToken?.type === "operator" && prevPrevToken.value === "\"";
|
|
14653
15585
|
if (state === "address") token.value = token.value.replace(/^[^<]*<\s*/, "");
|
|
14654
|
-
if (prevToken && prevToken.noBreak && data[state].length) {
|
|
15586
|
+
if (prevToken && prevToken.noBreak && data[state].length && (prevToken.value !== ")" || lastChars[state] === "@" || token.value.charAt(0) === "@")) {
|
|
14655
15587
|
data[state][data[state].length - 1] += token.value;
|
|
14656
15588
|
if (state === "text" && insideQuotes) data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
14657
15589
|
} else {
|
|
14658
15590
|
data[state].push(token.value);
|
|
14659
|
-
if (state === "text") data.textWasQuoted.push(insideQuotes);
|
|
15591
|
+
if (state === "text") data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
14660
15592
|
}
|
|
15593
|
+
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
14661
15594
|
}
|
|
14662
15595
|
}
|
|
14663
15596
|
if (!data.text.length && data.comment.length) {
|
|
@@ -14665,32 +15598,49 @@ function _handleAddress(tokens, depth) {
|
|
|
14665
15598
|
data.comment = [];
|
|
14666
15599
|
}
|
|
14667
15600
|
if (isGroup) {
|
|
14668
|
-
|
|
15601
|
+
const text = data.text.join(" ");
|
|
14669
15602
|
let groupMembers = [];
|
|
14670
|
-
if (data.group.length)
|
|
14671
|
-
if (member.group)
|
|
15603
|
+
if (data.group.length) parseAddressList$1(data.group.join(","), depth + 1).forEach((member) => {
|
|
15604
|
+
if (member.group) for (const nestedMember of member.group) groupMembers.push(nestedMember);
|
|
14672
15605
|
else groupMembers.push(member);
|
|
14673
15606
|
});
|
|
14674
15607
|
addresses.push({
|
|
14675
|
-
name: decodeWords(
|
|
15608
|
+
name: decodeWords(text),
|
|
14676
15609
|
group: groupMembers
|
|
14677
15610
|
});
|
|
14678
15611
|
} else {
|
|
15612
|
+
const addressComments = [];
|
|
15613
|
+
const addressParts = [];
|
|
15614
|
+
for (const part of data.address) {
|
|
15615
|
+
if (part.indexOf("(") < 0) {
|
|
15616
|
+
addressParts.push(part);
|
|
15617
|
+
continue;
|
|
15618
|
+
}
|
|
15619
|
+
const stripped = stripAddressComments(part);
|
|
15620
|
+
for (const comment of stripped.comments) addressComments.push(comment);
|
|
15621
|
+
if (stripped.address) addressParts.push(stripped.address);
|
|
15622
|
+
}
|
|
15623
|
+
data.address = addressParts;
|
|
14679
15624
|
if (!data.address.length && data.text.length) {
|
|
14680
|
-
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && data.text[i]
|
|
15625
|
+
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
|
|
14681
15626
|
data.address = data.text.splice(i, 1);
|
|
14682
15627
|
data.textWasQuoted.splice(i, 1);
|
|
14683
15628
|
break;
|
|
14684
15629
|
}
|
|
14685
15630
|
if (!data.address.length) {
|
|
14686
15631
|
for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i]) {
|
|
14687
|
-
const
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
15632
|
+
const part = data.text[i];
|
|
15633
|
+
const start = looseAddressStart(part);
|
|
15634
|
+
if (start >= 0) {
|
|
15635
|
+
LOOSE_TEXT_ADDR.lastIndex = start;
|
|
15636
|
+
const match = LOOSE_TEXT_ADDR.exec(part);
|
|
15637
|
+
if (match) {
|
|
15638
|
+
data.address = [match[0].trim()];
|
|
15639
|
+
data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
|
|
15640
|
+
break;
|
|
15641
|
+
}
|
|
14692
15642
|
}
|
|
14693
|
-
data.text[i] =
|
|
15643
|
+
data.text[i] = part.trim();
|
|
14694
15644
|
}
|
|
14695
15645
|
}
|
|
14696
15646
|
}
|
|
@@ -14699,25 +15649,33 @@ function _handleAddress(tokens, depth) {
|
|
|
14699
15649
|
data.comment = [];
|
|
14700
15650
|
}
|
|
14701
15651
|
if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
|
|
14702
|
-
data.
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
15652
|
+
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
|
|
15653
|
+
let text = data.text.join(" ");
|
|
15654
|
+
let addressValue = data.address.join(" ");
|
|
15655
|
+
if (!addressValue && isEncodedWordsOnly(text.trim())) {
|
|
15656
|
+
const decodedText = decodeWords(text);
|
|
15657
|
+
if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
|
|
15658
|
+
const parsedSubAddresses = parseAddressList$1(decodedText, depth + 1);
|
|
15659
|
+
if (parsedSubAddresses.length) return parsedSubAddresses;
|
|
14709
15660
|
}
|
|
14710
15661
|
return [{
|
|
14711
15662
|
address: "",
|
|
14712
15663
|
name: decodedText
|
|
14713
15664
|
}];
|
|
14714
15665
|
}
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
15666
|
+
if (addressFromQuotedText && text.indexOf("@") >= 0) {
|
|
15667
|
+
addressValue = quoteLocalPart(text);
|
|
15668
|
+
text = "";
|
|
15669
|
+
}
|
|
15670
|
+
({address: addressValue, text} = recoverAddrSpec(addressValue, text));
|
|
15671
|
+
if (!text && addressComments.length) text = addressComments.join(" ");
|
|
15672
|
+
const rawName = text || addressValue || "";
|
|
15673
|
+
const address = {
|
|
15674
|
+
address: addressValue || text || "",
|
|
15675
|
+
name: decodeWords(rawName)
|
|
14718
15676
|
};
|
|
14719
|
-
if (address.address === address.name) {
|
|
14720
|
-
if (
|
|
15677
|
+
if (address.address === rawName || address.address === address.name) {
|
|
15678
|
+
if (address.address.match(/@/)) address.name = "";
|
|
14721
15679
|
else address.address = "";
|
|
14722
15680
|
}
|
|
14723
15681
|
addresses.push(address);
|
|
@@ -14725,22 +15683,21 @@ function _handleAddress(tokens, depth) {
|
|
|
14725
15683
|
return addresses;
|
|
14726
15684
|
}
|
|
14727
15685
|
/**
|
|
14728
|
-
*
|
|
14729
|
-
*
|
|
14730
|
-
* @constructor
|
|
14731
|
-
* @param {String} str Address field string
|
|
15686
|
+
* Tokenizer for address field strings
|
|
14732
15687
|
*/
|
|
14733
15688
|
var Tokenizer$2 = class {
|
|
15689
|
+
/**
|
|
15690
|
+
* @param str Address field string
|
|
15691
|
+
*/
|
|
14734
15692
|
constructor(str) {
|
|
14735
15693
|
this.str = (str || "").toString();
|
|
14736
15694
|
this.operatorCurrent = "";
|
|
14737
15695
|
this.operatorExpecting = "";
|
|
14738
15696
|
this.node = null;
|
|
14739
15697
|
this.escaped = false;
|
|
15698
|
+
this.inDomainLiteral = false;
|
|
15699
|
+
this.afterAt = false;
|
|
14740
15700
|
this.list = [];
|
|
14741
|
-
/**
|
|
14742
|
-
* Operator tokens and which tokens are expected to end the sequence
|
|
14743
|
-
*/
|
|
14744
15701
|
this.operators = {
|
|
14745
15702
|
"\"": "\"",
|
|
14746
15703
|
"(": ")",
|
|
@@ -14753,7 +15710,7 @@ var Tokenizer$2 = class {
|
|
|
14753
15710
|
/**
|
|
14754
15711
|
* Tokenizes the original input string
|
|
14755
15712
|
*
|
|
14756
|
-
* @return
|
|
15713
|
+
* @return An array of operator|text tokens
|
|
14757
15714
|
*/
|
|
14758
15715
|
tokenize() {
|
|
14759
15716
|
let list = [];
|
|
@@ -14771,9 +15728,14 @@ var Tokenizer$2 = class {
|
|
|
14771
15728
|
/**
|
|
14772
15729
|
* Checks if a character is an operator or text and acts accordingly
|
|
14773
15730
|
*
|
|
14774
|
-
* @param
|
|
15731
|
+
* @param chr Character from the address field
|
|
15732
|
+
* @param nextChr The character that follows, or null at the end of the field
|
|
14775
15733
|
*/
|
|
14776
15734
|
checkChar(chr, nextChr) {
|
|
15735
|
+
if (!this.escaped && !this.operatorExpecting) {
|
|
15736
|
+
if (!this.inDomainLiteral && chr === "[" && this.afterAt) this.inDomainLiteral = true;
|
|
15737
|
+
else if (this.inDomainLiteral && (chr === "]" || chr === "," || chr === ";")) this.inDomainLiteral = false;
|
|
15738
|
+
}
|
|
14777
15739
|
if (this.escaped) {} else if (chr === this.operatorExpecting) {
|
|
14778
15740
|
this.node = {
|
|
14779
15741
|
type: "operator",
|
|
@@ -14789,20 +15751,22 @@ var Tokenizer$2 = class {
|
|
|
14789
15751
|
].includes(nextChr)) this.node.noBreak = true;
|
|
14790
15752
|
this.list.push(this.node);
|
|
14791
15753
|
this.node = null;
|
|
15754
|
+
if (chr !== ")") this.afterAt = false;
|
|
14792
15755
|
this.operatorExpecting = "";
|
|
14793
15756
|
this.escaped = false;
|
|
14794
15757
|
return;
|
|
14795
|
-
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
15758
|
+
} else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ":") && chr in this.operators) {
|
|
14796
15759
|
this.node = {
|
|
14797
15760
|
type: "operator",
|
|
14798
15761
|
value: chr
|
|
14799
15762
|
};
|
|
14800
15763
|
this.list.push(this.node);
|
|
14801
15764
|
this.node = null;
|
|
15765
|
+
if (chr !== "(") this.afterAt = false;
|
|
14802
15766
|
this.operatorExpecting = this.operators[chr];
|
|
14803
15767
|
this.escaped = false;
|
|
14804
15768
|
return;
|
|
14805
|
-
} else if (this.operatorExpecting === "\"" && chr === "\\") {
|
|
15769
|
+
} else if ((this.operatorExpecting === "\"" || this.operatorExpecting === ")") && chr === "\\") {
|
|
14806
15770
|
this.escaped = true;
|
|
14807
15771
|
return;
|
|
14808
15772
|
}
|
|
@@ -14814,7 +15778,10 @@ var Tokenizer$2 = class {
|
|
|
14814
15778
|
this.list.push(this.node);
|
|
14815
15779
|
}
|
|
14816
15780
|
if (chr === "\n") chr = " ";
|
|
14817
|
-
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr))
|
|
15781
|
+
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
|
|
15782
|
+
this.node.value += chr;
|
|
15783
|
+
if (!this.operatorExpecting && chr !== " " && chr !== " ") this.afterAt = chr === "@";
|
|
15784
|
+
}
|
|
14818
15785
|
this.escaped = false;
|
|
14819
15786
|
}
|
|
14820
15787
|
};
|
|
@@ -14825,29 +15792,20 @@ var Tokenizer$2 = class {
|
|
|
14825
15792
|
*/
|
|
14826
15793
|
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
14827
15794
|
/**
|
|
14828
|
-
* Parses
|
|
14829
|
-
*
|
|
14830
|
-
*
|
|
14831
|
-
*
|
|
14832
|
-
* 'Name <address@domain>'
|
|
15795
|
+
* Parses an address list, recursing into groups. The depth is threaded through the
|
|
15796
|
+
* calls rather than taken from an option, so a caller supplied options object can not
|
|
15797
|
+
* seed it and lift the recursion limit.
|
|
14833
15798
|
*
|
|
14834
|
-
*
|
|
14835
|
-
*
|
|
14836
|
-
*
|
|
14837
|
-
*
|
|
14838
|
-
* @param {String} str Address field
|
|
14839
|
-
* @param {Object} options Optional options object
|
|
14840
|
-
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
|
|
14841
|
-
* @return {Array} An array of address objects
|
|
15799
|
+
* @param str Address field
|
|
15800
|
+
* @param depth Current recursion depth for nested group protection
|
|
15801
|
+
* @return An array of address objects
|
|
14842
15802
|
*/
|
|
14843
|
-
function
|
|
14844
|
-
options = options || {};
|
|
14845
|
-
let depth = options._depth || 0;
|
|
15803
|
+
function parseAddressList$1(str, depth) {
|
|
14846
15804
|
if (depth > MAX_NESTED_GROUP_DEPTH) return [];
|
|
14847
15805
|
let tokens = new Tokenizer$2(str).tokenize();
|
|
14848
15806
|
let addresses = [];
|
|
14849
15807
|
let address = [];
|
|
14850
|
-
|
|
15808
|
+
const parsedAddresses = [];
|
|
14851
15809
|
tokens.forEach((token) => {
|
|
14852
15810
|
if (token.type === "operator" && (token.value === "," || token.value === ";")) {
|
|
14853
15811
|
if (address.length) addresses.push(address);
|
|
@@ -14855,11 +15813,34 @@ function addressParser(str, options) {
|
|
|
14855
15813
|
} else address.push(token);
|
|
14856
15814
|
});
|
|
14857
15815
|
if (address.length) addresses.push(address);
|
|
14858
|
-
addresses.
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
15816
|
+
for (const tokens of addresses) for (const address of _handleAddress(tokens, depth)) parsedAddresses.push(address);
|
|
15817
|
+
const mergedAddresses = [];
|
|
15818
|
+
for (let i = parsedAddresses.length - 1; i >= 0; i--) {
|
|
15819
|
+
const current = parsedAddresses[i];
|
|
15820
|
+
const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
|
|
15821
|
+
if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
|
|
15822
|
+
else mergedAddresses.push(current);
|
|
15823
|
+
}
|
|
15824
|
+
return mergedAddresses.reverse();
|
|
15825
|
+
}
|
|
15826
|
+
/**
|
|
15827
|
+
* Parses structured e-mail addresses from an address field
|
|
15828
|
+
*
|
|
15829
|
+
* Example:
|
|
15830
|
+
*
|
|
15831
|
+
* 'Name <address@domain>'
|
|
15832
|
+
*
|
|
15833
|
+
* will be converted to
|
|
15834
|
+
*
|
|
15835
|
+
* [{name: 'Name', address: 'address@domain'}]
|
|
15836
|
+
*
|
|
15837
|
+
* @param str Address field
|
|
15838
|
+
* @param options Optional options object
|
|
15839
|
+
* @return An array of address objects
|
|
15840
|
+
*/
|
|
15841
|
+
function addressParser(str, options) {
|
|
15842
|
+
const parsedAddresses = parseAddressList$1(str, 0);
|
|
15843
|
+
if (options && options.flatten) {
|
|
14863
15844
|
let addresses = [];
|
|
14864
15845
|
let walkAddressList = (list) => {
|
|
14865
15846
|
list.forEach((address) => {
|
|
@@ -14872,6 +15853,7 @@ function addressParser(str, options) {
|
|
|
14872
15853
|
}
|
|
14873
15854
|
return parsedAddresses;
|
|
14874
15855
|
}
|
|
15856
|
+
new Uint8Array([10]);
|
|
14875
15857
|
//#endregion
|
|
14876
15858
|
//#region ../../node_modules/.pnpm/domelementtype@3.0.0/node_modules/domelementtype/dist/index.js
|
|
14877
15859
|
/** Types of elements found in htmlparser2's DOM */
|
|
@@ -25992,7 +26974,7 @@ function bad(message, hint) {
|
|
|
25992
26974
|
/** A bare address, checked and canonicalised. Display names are only accepted on From. */
|
|
25993
26975
|
function address(value, field) {
|
|
25994
26976
|
const trimmed = value.trim();
|
|
25995
|
-
if (!ADDRESS.test(trimmed)) throw bad(`${field}
|
|
26977
|
+
if (!ADDRESS.test(trimmed)) throw bad(`${field} contains a value that is not a bare email address`, "Give bare addresses.");
|
|
25996
26978
|
return canonicalAddress(trimmed);
|
|
25997
26979
|
}
|
|
25998
26980
|
function list(values, field) {
|
|
@@ -27778,11 +28760,37 @@ function outcomeUnavailable(why) {
|
|
|
27778
28760
|
//#endregion
|
|
27779
28761
|
//#region src/operations/send.ts
|
|
27780
28762
|
/**
|
|
28763
|
+
* A caller may hand back the canonical expectation it kept, or the display-safe `expect` that prepare returned. The
|
|
28764
|
+
* latter is accepted only when it is byte-for-byte this approval's field rendering under the boundary it carries, the
|
|
28765
|
+
* way Gmail accepts its own; then the claim compares the raw binding. Anything else is passed on as given, so an
|
|
28766
|
+
* invented or edited envelope still mismatches and the send is refused.
|
|
28767
|
+
*/
|
|
28768
|
+
function canonicalSuppliedExpectation(supplied, bound, account, id) {
|
|
28769
|
+
const boundary = /^<untrusted-content boundary="([A-Za-z0-9_-]+)" field="subject"/.exec(supplied.subject)?.[1];
|
|
28770
|
+
if (boundary === void 0) return supplied;
|
|
28771
|
+
const displayed = expectationFields(bound, {
|
|
28772
|
+
boundary,
|
|
28773
|
+
account,
|
|
28774
|
+
id
|
|
28775
|
+
});
|
|
28776
|
+
const same = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
28777
|
+
return displayed.subject === supplied.subject && same(displayed.to, supplied.to) && same(displayed.cc, supplied.cc) && same(displayed.bcc, supplied.bcc) ? bound : supplied;
|
|
28778
|
+
}
|
|
28779
|
+
/**
|
|
27781
28780
|
* A used approval, as Resend's own surfaces refuse it (design 2026-10-05 §D2, the used-send row): Resend accepted the
|
|
27782
28781
|
* email — which is not that it went, since one may be scheduled, bounce or be cancelled — and never "sent". The words
|
|
27783
28782
|
* `resend_send_wait` shows for it too, core's for every provider.
|
|
27784
28783
|
*/
|
|
27785
28784
|
const ACCEPTED_BY_RESEND = (usedAt) => `accepted by Resend at ${usedAt}`;
|
|
28785
|
+
/** A send's recipients and subject as Resend may return them: display-safe, never the raw approval binding. */
|
|
28786
|
+
function expectationFields(expect, envelope) {
|
|
28787
|
+
return {
|
|
28788
|
+
to: expect.to.map((address) => addressField(address, "to", envelope)),
|
|
28789
|
+
cc: expect.cc.map((address) => addressField(address, "cc", envelope)),
|
|
28790
|
+
bcc: expect.bcc.map((address) => addressField(address, "bcc", envelope)),
|
|
28791
|
+
subject: wrapField(expect.subject, "subject", envelope)
|
|
28792
|
+
};
|
|
28793
|
+
}
|
|
27786
28794
|
/**
|
|
27787
28795
|
* A send's approval as the one account that may use it, classified under its lock as the claim would classify it
|
|
27788
28796
|
* (design 2026-10-05 §D2), or a refusal that does not touch it.
|
|
@@ -28043,9 +29051,13 @@ async function prepareSend(context, name, input) {
|
|
|
28043
29051
|
effectivePolicy: effective,
|
|
28044
29052
|
riskFlags: study.flags,
|
|
28045
29053
|
reach,
|
|
28046
|
-
expect: record.expect,
|
|
29054
|
+
expect: expectationFields(record.expect, {
|
|
29055
|
+
boundary: newBoundary(),
|
|
29056
|
+
account: name,
|
|
29057
|
+
id: record.approvalId
|
|
29058
|
+
}),
|
|
28047
29059
|
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
|
|
29060
|
+
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
29061
|
approval: await context.core.approvals.approvalOf(record),
|
|
28050
29062
|
...legacyDrain === void 0 ? {} : { legacyDrain }
|
|
28051
29063
|
};
|
|
@@ -28236,7 +29248,7 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
|
|
|
28236
29248
|
contentDigest: digest,
|
|
28237
29249
|
inboxId: named.account.id,
|
|
28238
29250
|
inboxSub: named.account.userId,
|
|
28239
|
-
expect: options.expect
|
|
29251
|
+
expect: options.expect === void 0 ? known.expect : canonicalSuppliedExpectation(options.expect, known.expect, name, known.approvalId)
|
|
28240
29252
|
}, {
|
|
28241
29253
|
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
29254
|
usedSaid: ACCEPTED_BY_RESEND,
|
|
@@ -28396,16 +29408,26 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
|
|
|
28396
29408
|
state: message.scheduledAt ? "scheduled" : "sent",
|
|
28397
29409
|
scheduledAt: message.scheduledAt,
|
|
28398
29410
|
said: acceptedSaid(resendId !== void 0, message.scheduledAt),
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
29411
|
+
...expectationFields(claimed.expect, {
|
|
29412
|
+
boundary: newBoundary(),
|
|
29413
|
+
account: name,
|
|
29414
|
+
id: options.approvalId
|
|
29415
|
+
}),
|
|
28403
29416
|
...unrecorded.length > 0 ? { note: unrecorded.join("; ") } : {},
|
|
28404
29417
|
approval: approval ?? await approvalNow(context, options.approvalId, named.account.id, claimedApproval),
|
|
28405
29418
|
...legacyDrain === void 0 ? {} : { legacyDrain }
|
|
28406
29419
|
};
|
|
28407
29420
|
});
|
|
28408
29421
|
}
|
|
29422
|
+
/** A stored send as a result may show it: canonical recipients stay addresses; its subject is always prose. */
|
|
29423
|
+
function sendSummaryFields(summary, envelope) {
|
|
29424
|
+
if (summary === null) return null;
|
|
29425
|
+
return {
|
|
29426
|
+
...summary,
|
|
29427
|
+
recipients: summary.recipients.map((address) => addressField(address, "recipient", envelope)),
|
|
29428
|
+
...summary.subject === void 0 ? {} : { subject: wrapField(summary.subject, "subject", envelope) }
|
|
29429
|
+
};
|
|
29430
|
+
}
|
|
28409
29431
|
const CANNOT_READ = "this key can only send, so it cannot read what Resend did with the email";
|
|
28410
29432
|
/**
|
|
28411
29433
|
* What this machine knows Resend accepted, from its own send record: a scheduled send is accepted for the time the
|
|
@@ -28439,11 +29461,16 @@ async function sendStatus(context, name, approvalId) {
|
|
|
28439
29461
|
throw error;
|
|
28440
29462
|
});
|
|
28441
29463
|
const stored = seen?.stored ?? null;
|
|
29464
|
+
const envelope = {
|
|
29465
|
+
boundary: newBoundary(),
|
|
29466
|
+
account: name,
|
|
29467
|
+
id: approvalId
|
|
29468
|
+
};
|
|
28442
29469
|
const base = {
|
|
28443
29470
|
account: name,
|
|
28444
29471
|
approvalId,
|
|
28445
|
-
approval: seen ? publicApproval(seen.stored, seen.outcome) : null,
|
|
28446
|
-
local
|
|
29472
|
+
approval: seen ? publicApproval(seen.stored, seen.outcome, { wrap: (text, field) => field === "subject" || field === "filename" ? wrapField(text, field, envelope) : addressField(text, field, envelope) }) : null,
|
|
29473
|
+
local: sendSummaryFields(local, envelope)
|
|
28447
29474
|
};
|
|
28448
29475
|
if (!local) return {
|
|
28449
29476
|
...base,
|