@agentcomms/resend 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_LICENSES +2 -2
- package/dist/cli.mjs +3 -3
- package/dist/{coerce-CqGigABD.mjs → coerce-DUN95oU4.mjs} +3 -3
- package/dist/{dist-C7DG2LdL-l0mj9_dH.mjs → dist-C7DG2LdL-6QTVBgXp.mjs} +1 -1
- package/dist/index.d.mts +1012 -3047
- package/dist/index.mjs +2 -14
- package/dist/{iso-USuVU_sk.mjs → iso-5-9R47LC.mjs} +59 -26
- package/dist/{mcp-DIH4cS6P-CIWWZQbz.mjs → mcp-DIH4cS6P-C1s6ZIjc.mjs} +2 -2
- package/dist/{send-1murS6En.mjs → send-BjDliNlb.mjs} +1493 -394
- package/dist/{server-COls8j1E.mjs → server-D2WXrDVG.mjs} +5 -5
- package/dist/{src-WCy6ifGf-DXrNj-15-D0oRtifb.mjs → src-WCy6ifGf-DXrNj-15-HcB9fGcS.mjs} +2 -2
- package/dist/{stdio-B8UXEGiG.mjs → stdio-BWbiQh9h.mjs} +1 -1
- package/dist/{stdio-CyjIgrch-59zcjSEt.mjs → stdio-CRbkOCqe-BpzkpnG6.mjs} +15 -15
- package/dist/{stdio-entry-EutVt-Bl.mjs → stdio-entry-C9gpIGSo.mjs} +1 -1
- package/package.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-
|
|
1
|
+
import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
|
|
2
2
|
import { access, chmod, constants, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
|
|
3
3
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -9,10 +9,11 @@ import { styleText } from "node:util";
|
|
|
9
9
|
import { homedir, platform, userInfo } from "node:os";
|
|
10
10
|
import { accessSync, constants as constants$1, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
11
11
|
import { execFile, spawn } from "node:child_process";
|
|
12
|
+
import { gunzipSync } from "node:zlib";
|
|
12
13
|
//#endregion
|
|
13
14
|
//#region src/version.ts
|
|
14
15
|
/** The package version, read from package.json at build time. */
|
|
15
|
-
const VERSION$2 = "0.
|
|
16
|
+
const VERSION$2 = "0.26.1";
|
|
16
17
|
//#endregion
|
|
17
18
|
//#region src/api/routes.ts
|
|
18
19
|
/**
|
|
@@ -201,7 +202,7 @@ const RESEND_CALLER = Object.freeze({
|
|
|
201
202
|
packageName: PACKAGE_NAME
|
|
202
203
|
});
|
|
203
204
|
//#endregion
|
|
204
|
-
//#region ../core/dist/update-check-
|
|
205
|
+
//#region ../core/dist/update-check-CfybSI56.mjs
|
|
205
206
|
/**
|
|
206
207
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
207
208
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -4447,9 +4448,6 @@ function isBehind(version, latest) {
|
|
|
4447
4448
|
function isPlainVersion(value) {
|
|
4448
4449
|
return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
|
|
4449
4450
|
}
|
|
4450
|
-
function isPrerelease(version) {
|
|
4451
|
-
return VERSION_PATTERN.exec(version)?.[4] !== void 0;
|
|
4452
|
-
}
|
|
4453
4451
|
/**
|
|
4454
4452
|
* Whether `version` is within a package's `engines` range: true or false, or null when the range is not one this reads.
|
|
4455
4453
|
*
|
|
@@ -5583,8 +5581,8 @@ async function mergeIntoJsonConfig(path, client, name, entry) {
|
|
|
5583
5581
|
}
|
|
5584
5582
|
/** Starts the server exactly as a client would, and completes the handshake. */
|
|
5585
5583
|
async function verifyEntry(entry, product) {
|
|
5586
|
-
const { Client } = await import("./dist-C7DG2LdL-
|
|
5587
|
-
const { StdioClientTransport } = await import("./stdio-
|
|
5584
|
+
const { Client } = await import("./dist-C7DG2LdL-6QTVBgXp.mjs");
|
|
5585
|
+
const { StdioClientTransport } = await import("./stdio-CRbkOCqe-BpzkpnG6.mjs");
|
|
5588
5586
|
const client = new Client({
|
|
5589
5587
|
name: `${product.binary}-install-check`,
|
|
5590
5588
|
version: product.version
|
|
@@ -12998,7 +12996,7 @@ function folding(platform) {
|
|
|
12998
12996
|
return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
|
|
12999
12997
|
}
|
|
13000
12998
|
/** The package version, read from package.json at build time. */
|
|
13001
|
-
const VERSION = "0.
|
|
12999
|
+
const VERSION = "0.26.1";
|
|
13002
13000
|
/**
|
|
13003
13001
|
* The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
|
|
13004
13002
|
*
|
|
@@ -14371,7 +14369,7 @@ const PER_WORKSPACE = {
|
|
|
14371
14369
|
"agentLabel.slackStyles": "app",
|
|
14372
14370
|
"agentLabel.slackIcons": "your Slack photo"
|
|
14373
14371
|
};
|
|
14374
|
-
function recordOf$
|
|
14372
|
+
function recordOf$2(value) {
|
|
14375
14373
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
14376
14374
|
}
|
|
14377
14375
|
/**
|
|
@@ -14381,7 +14379,7 @@ function recordOf$1(value) {
|
|
|
14381
14379
|
*/
|
|
14382
14380
|
function perWorkspace(field, before, after) {
|
|
14383
14381
|
const none = PER_WORKSPACE[field];
|
|
14384
|
-
const [was, now] = [recordOf$
|
|
14382
|
+
const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
|
|
14385
14383
|
if (none === void 0 || was === void 0 || now === void 0) return void 0;
|
|
14386
14384
|
const value = (record, name) => Object.hasOwn(record, name) ? String(record[name]) : none;
|
|
14387
14385
|
const moved = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].filter((name) => value(was, name) !== value(now, name)).sort();
|
|
@@ -14394,7 +14392,7 @@ function perWorkspace(field, before, after) {
|
|
|
14394
14392
|
* commas. A level one side does not list allows everything there. Undefined for a value of another shape.
|
|
14395
14393
|
*/
|
|
14396
14394
|
function perLevel(field, before, after) {
|
|
14397
|
-
const [was, now] = [recordOf$
|
|
14395
|
+
const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
|
|
14398
14396
|
if (field !== "behaviour.rules" || was === void 0 || now === void 0) return void 0;
|
|
14399
14397
|
const value = (record, level) => Object.hasOwn(record, level) ? String(record[level]) : "everything";
|
|
14400
14398
|
const levels = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].sort();
|
|
@@ -15012,7 +15010,7 @@ const OUTCOMES = [
|
|
|
15012
15010
|
"unknown"
|
|
15013
15011
|
];
|
|
15014
15012
|
const isString = (value) => typeof value === "string";
|
|
15015
|
-
const isTime$
|
|
15013
|
+
const isTime$3 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
|
|
15016
15014
|
/** One line as an entry, or null when it is not one this release wrote. */
|
|
15017
15015
|
function entryOf(line) {
|
|
15018
15016
|
let raw;
|
|
@@ -15023,7 +15021,7 @@ function entryOf(line) {
|
|
|
15023
15021
|
}
|
|
15024
15022
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
15025
15023
|
const entry = raw;
|
|
15026
|
-
if (!isTime$
|
|
15024
|
+
if (!isTime$3(entry.at)) return null;
|
|
15027
15025
|
if (entry.kind === "use") {
|
|
15028
15026
|
if (![
|
|
15029
15027
|
"standingId",
|
|
@@ -15055,7 +15053,7 @@ function entryOf(line) {
|
|
|
15055
15053
|
if (entry.kind === "outcome") {
|
|
15056
15054
|
if (!isString(entry.childId) || !isString(entry.outcome) || !OUTCOMES.includes(entry.outcome)) return null;
|
|
15057
15055
|
if (!isString(entry.notice) || entry.link !== void 0 && !isString(entry.link)) return null;
|
|
15058
|
-
if (entry.actAt !== void 0 && !isTime$
|
|
15056
|
+
if (entry.actAt !== void 0 && !isTime$3(entry.actAt)) return null;
|
|
15059
15057
|
return {
|
|
15060
15058
|
kind: "outcome",
|
|
15061
15059
|
at: entry.at,
|
|
@@ -15500,7 +15498,7 @@ function updateJobSupported(platform) {
|
|
|
15500
15498
|
function updateJobMarkerPath(stateDir) {
|
|
15501
15499
|
return join(stateDir, "update-job.json");
|
|
15502
15500
|
}
|
|
15503
|
-
const isTime$
|
|
15501
|
+
const isTime$2 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
15504
15502
|
/** The record, or null when there is none — or none this release can read, which is treated as none. */
|
|
15505
15503
|
async function readUpdateJobMarker(stateDir) {
|
|
15506
15504
|
let parsed;
|
|
@@ -15515,11 +15513,11 @@ async function readUpdateJobMarker(stateDir) {
|
|
|
15515
15513
|
platform: record.platform,
|
|
15516
15514
|
installedAt: record.installedAt,
|
|
15517
15515
|
label: record.label,
|
|
15518
|
-
...isTime$
|
|
15516
|
+
...isTime$2(record.lastRun) ? { lastRun: record.lastRun } : {}
|
|
15519
15517
|
};
|
|
15520
15518
|
}
|
|
15521
15519
|
function within$1(at, now, intervalMs) {
|
|
15522
|
-
if (at === void 0 || !isTime$
|
|
15520
|
+
if (at === void 0 || !isTime$2(at)) return false;
|
|
15523
15521
|
const since = now.getTime() - Date.parse(at);
|
|
15524
15522
|
return since >= -3e5 && since < intervalMs;
|
|
15525
15523
|
}
|
|
@@ -15600,7 +15598,7 @@ const NOTICE_OUTCOMES = [
|
|
|
15600
15598
|
function readBackgroundNotice(value) {
|
|
15601
15599
|
if (typeof value !== "object" || value === null) return null;
|
|
15602
15600
|
const { at, outcome, version, text } = value;
|
|
15603
|
-
if (!isTime(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
|
|
15601
|
+
if (!isTime$1(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
|
|
15604
15602
|
return {
|
|
15605
15603
|
at,
|
|
15606
15604
|
outcome,
|
|
@@ -15629,7 +15627,7 @@ const EMPTY_UPDATE_CHECK = Object.freeze({
|
|
|
15629
15627
|
function updateCheckPath(stateDir) {
|
|
15630
15628
|
return join(stateDir, UPDATE_CHECK_FILE);
|
|
15631
15629
|
}
|
|
15632
|
-
const isTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
15630
|
+
const isTime$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
15633
15631
|
const isWords = (value) => Array.isArray(value) && value.every((word) => typeof word === "string");
|
|
15634
15632
|
function readRelease(value) {
|
|
15635
15633
|
if (typeof value !== "object" || value === null) return null;
|
|
@@ -15662,17 +15660,17 @@ async function readUpdateCheck(stateDir) {
|
|
|
15662
15660
|
}
|
|
15663
15661
|
const held = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
15664
15662
|
return {
|
|
15665
|
-
lastChecked: isTime(held.lastChecked) ? held.lastChecked : null,
|
|
15663
|
+
lastChecked: isTime$1(held.lastChecked) ? held.lastChecked : null,
|
|
15666
15664
|
latest: isPlainVersion(held.latest) ? held.latest : null,
|
|
15667
15665
|
behind: typeof held.behind === "boolean" ? held.behind : null,
|
|
15668
15666
|
current: readCurrent(held.current),
|
|
15669
15667
|
lastError: typeof held.lastError === "string" ? held.lastError.slice(0, 300) : null,
|
|
15670
|
-
snoozedUntil: isTime(held.snoozedUntil) ? held.snoozedUntil : null,
|
|
15671
|
-
checking: isTime(held.checking) ? held.checking : null,
|
|
15672
|
-
commandNoticeAt: isTime(held.commandNoticeAt) ? held.commandNoticeAt : null,
|
|
15673
|
-
noticeAt: isTime(held.noticeAt) ? held.noticeAt : null,
|
|
15674
|
-
pausedAt: isTime(held.pausedAt) ? held.pausedAt : null,
|
|
15675
|
-
pausedUntil: isTime(held.pausedUntil) ? held.pausedUntil : null,
|
|
15668
|
+
snoozedUntil: isTime$1(held.snoozedUntil) ? held.snoozedUntil : null,
|
|
15669
|
+
checking: isTime$1(held.checking) ? held.checking : null,
|
|
15670
|
+
commandNoticeAt: isTime$1(held.commandNoticeAt) ? held.commandNoticeAt : null,
|
|
15671
|
+
noticeAt: isTime$1(held.noticeAt) ? held.noticeAt : null,
|
|
15672
|
+
pausedAt: isTime$1(held.pausedAt) ? held.pausedAt : null,
|
|
15673
|
+
pausedUntil: isTime$1(held.pausedUntil) ? held.pausedUntil : null,
|
|
15676
15674
|
release: readRelease(held.release),
|
|
15677
15675
|
securityAsked: isPlainVersion(held.securityAsked) ? held.securityAsked : null,
|
|
15678
15676
|
securityShown: readSecurityShown(held.securityShown),
|
|
@@ -15681,11 +15679,41 @@ async function readUpdateCheck(stateDir) {
|
|
|
15681
15679
|
};
|
|
15682
15680
|
}
|
|
15683
15681
|
/**
|
|
15682
|
+
* How every writer of the record takes the lock beside it: 2 s for each holder, and 15 s in all (design 2026-10-22, R2).
|
|
15683
|
+
*
|
|
15684
|
+
* Every server's tool call takes this lock for its refresh, so on a busy machine a writer often waits behind several
|
|
15685
|
+
* holders in turn, each holding it for a moment. Counted from the start of the wait, 2 s was a budget for that whole
|
|
15686
|
+
* queue, and a writer at the back of it gave up while the lock was being handed on exactly as it should be: a
|
|
15687
|
+
* person's update was installed and its record dropped. Counted per holder, a writer gives up only on one holder that
|
|
15688
|
+
* kept the lock for 2 s, which is a stuck lock; 15 s bounds a queue that never empties, as config writes are bounded.
|
|
15689
|
+
*/
|
|
15690
|
+
const UPDATE_CHECK_LOCK = Object.freeze({
|
|
15691
|
+
timeoutPerHolder: true,
|
|
15692
|
+
timeoutMs: 2e3,
|
|
15693
|
+
maxWaitMs: 15e3
|
|
15694
|
+
});
|
|
15695
|
+
/**
|
|
15696
|
+
* How a notice takes the lock: 2 s in all, counted from the start of the wait, however often it changes hands meanwhile.
|
|
15697
|
+
*
|
|
15698
|
+
* A notice is best effort. Deciding a tool call's or a command's line, the security line in a preview, and recording
|
|
15699
|
+
* the person's answer to that line take the lock only for what is said, and each runs while a person or an agent
|
|
15700
|
+
* waits. Waited out holder by holder, as `UPDATE_CHECK_LOCK` waits, a busy machine's queue of refreshes held them for
|
|
15701
|
+
* up to 15 s. Busy past 2 s, a notice decides nothing this time, and a later call says it.
|
|
15702
|
+
*
|
|
15703
|
+
* Claiming the day's check is best effort too: busy past 2 s, nothing is claimed, and a later process claims it (0.26.0
|
|
15704
|
+
* review B). The claim's function is not named here: this comment ships in WhatsApp's bundle, which carries none of the
|
|
15705
|
+
* checker, not even its name (`packages/whatsapp/test/update-gate.test.ts`).
|
|
15706
|
+
*/
|
|
15707
|
+
const UPDATE_NOTICE_LOCK = Object.freeze({ timeoutMs: 2e3 });
|
|
15708
|
+
/**
|
|
15684
15709
|
* Reads the record, changes it and writes it back, under a lock beside it, atomically — so a server recording a check
|
|
15685
15710
|
* and a person's "not now" arriving at the same moment each keep what the other wrote. Returns what `change` returned
|
|
15686
15711
|
* alongside the record written; `change` returning `null` writes nothing.
|
|
15712
|
+
*
|
|
15713
|
+
* `lock` is how long to wait for the lock: `UPDATE_CHECK_LOCK`, for a record that matters, unless a notice or the
|
|
15714
|
+
* day's claim passes `UPDATE_NOTICE_LOCK`. Busy past it, this throws LOCK_TIMEOUT, and nothing is written.
|
|
15687
15715
|
*/
|
|
15688
|
-
async function changeUpdateCheck(stateDir, change) {
|
|
15716
|
+
async function changeUpdateCheck(stateDir, change, lock = UPDATE_CHECK_LOCK) {
|
|
15689
15717
|
return withFileLock(join(stateDir, ".update-check.lock"), async () => {
|
|
15690
15718
|
const current = await readUpdateCheck(stateDir);
|
|
15691
15719
|
const next = change(current);
|
|
@@ -15700,7 +15728,7 @@ async function changeUpdateCheck(stateDir, change) {
|
|
|
15700
15728
|
result: next.result,
|
|
15701
15729
|
written: true
|
|
15702
15730
|
};
|
|
15703
|
-
},
|
|
15731
|
+
}, lock);
|
|
15704
15732
|
}
|
|
15705
15733
|
/**
|
|
15706
15734
|
* Whether the check is due: never asked, asked a day or more ago, or — a clock that went back — asked more than a few
|
|
@@ -15960,7 +15988,7 @@ async function securityAnswered(stateDir, approvalId) {
|
|
|
15960
15988
|
securityAsked: record.securityShown.version,
|
|
15961
15989
|
securityShown: null
|
|
15962
15990
|
} };
|
|
15963
|
-
});
|
|
15991
|
+
}, UPDATE_NOTICE_LOCK);
|
|
15964
15992
|
} catch {}
|
|
15965
15993
|
}
|
|
15966
15994
|
/** A local time as a person reads it in a preview: `2026-09-29 00:00`. */
|
|
@@ -16522,34 +16550,47 @@ function releaseIntegrity(packument, version) {
|
|
|
16522
16550
|
return typeof integrity === "string" && integrity.trim() !== "" ? integrity : null;
|
|
16523
16551
|
}
|
|
16524
16552
|
/**
|
|
16525
|
-
* What a release's core manifest declares under `agentcommsRelease` (U5): a security fix,
|
|
16526
|
-
*
|
|
16527
|
-
*
|
|
16528
|
-
* decision
|
|
16553
|
+
* What a release's core manifest declares under `agentcommsRelease` (U5, design 2026-10-22 §8.1): a security fix, a
|
|
16554
|
+
* decision it needs from the person, and the newest release up to it that needed one — as flags and a plain version
|
|
16555
|
+
* only. The manifest is the one in the release's verified tarball (`release-declarations.ts`, design 2026-10-22 P1),
|
|
16556
|
+
* never the registry's copy. The decision's own words are never read out: an update says that one is needed in its own
|
|
16557
|
+
* words. A declaration this release cannot read waits for the person, as it may be the decision it cannot see: a field
|
|
16558
|
+
* that is not an object, a `needsDecision` that is not false, or a `needsDecisionSince` that is not a plain version.
|
|
16529
16559
|
*/
|
|
16530
16560
|
function releaseDeclaration(manifest) {
|
|
16531
16561
|
const declared = manifest?.agentcommsRelease;
|
|
16532
16562
|
if (declared === void 0 || declared === null) return {
|
|
16533
16563
|
security: false,
|
|
16534
|
-
needsDecision: false
|
|
16564
|
+
needsDecision: false,
|
|
16565
|
+
needsDecisionSince: null
|
|
16535
16566
|
};
|
|
16536
16567
|
if (typeof declared !== "object" || Array.isArray(declared)) return {
|
|
16537
16568
|
security: false,
|
|
16538
|
-
needsDecision: true
|
|
16569
|
+
needsDecision: true,
|
|
16570
|
+
needsDecisionSince: null
|
|
16539
16571
|
};
|
|
16540
|
-
const { security, needsDecision } = declared;
|
|
16572
|
+
const { security, needsDecision, needsDecisionSince } = declared;
|
|
16573
|
+
const since = isPlainVersion(needsDecisionSince) ? needsDecisionSince : null;
|
|
16541
16574
|
return {
|
|
16542
16575
|
security: security === true,
|
|
16543
|
-
needsDecision: !(needsDecision === void 0 || needsDecision === null || needsDecision === false)
|
|
16576
|
+
needsDecision: needsDecisionSince !== void 0 && since === null || !(needsDecision === void 0 || needsDecision === null || needsDecision === false),
|
|
16577
|
+
needsDecisionSince: since
|
|
16544
16578
|
};
|
|
16545
16579
|
}
|
|
16546
16580
|
/**
|
|
16547
|
-
*
|
|
16548
|
-
*
|
|
16549
|
-
*
|
|
16581
|
+
* The release a machine running `from` must decide on before it runs `declared` (design 2026-10-22 §8.1), or null when
|
|
16582
|
+
* there is none: `declared` itself when it declares `needsDecision`; otherwise the release its `needsDecisionSince`
|
|
16583
|
+
* names, when that is after `from` — a decision this machine has not taken. One at or before `from` was taken when
|
|
16584
|
+
* that release was installed. Nothing else counts: no release the registry lists, hidden or added, decides anything.
|
|
16585
|
+
* A `from` that is not known, or not a version, cannot be shown to be past it, so it waits; a `since` after
|
|
16586
|
+
* `declared`, which no release check lets through, is said as `declared`.
|
|
16550
16587
|
*/
|
|
16551
|
-
function
|
|
16552
|
-
|
|
16588
|
+
function decisionNeeded(declared, from) {
|
|
16589
|
+
if (declared.needsDecision) return declared.version;
|
|
16590
|
+
const since = declared.needsDecisionSince;
|
|
16591
|
+
if (since === null) return null;
|
|
16592
|
+
if (from !== null && (compareVersions(since, from) ?? 1) <= 0) return null;
|
|
16593
|
+
return (compareVersions(since, declared.version) ?? 1) > 0 ? declared.version : since;
|
|
16553
16594
|
}
|
|
16554
16595
|
/** The oldest of `versions` that are versions, or null. */
|
|
16555
16596
|
function oldestVersion(versions) {
|
|
@@ -16653,42 +16694,6 @@ async function verifyReleaseSet(sources, wanted, act = "update", noVerifierHint)
|
|
|
16653
16694
|
return verified;
|
|
16654
16695
|
}
|
|
16655
16696
|
/**
|
|
16656
|
-
* What every release after `from`, up to `latest`, declares (design 2026-10-16 U5, U7; security review of 0.20.0,
|
|
16657
|
-
* severity 6): a decision any of them needs — a later release that does not say so again does not end it — and whether
|
|
16658
|
-
* `latest` is a security fix. Each declaration acted on is verified first, with `verify`; one that does not verify, or
|
|
16659
|
-
* where nothing here can verify (`verify` null: a channel's process, which carries no Sigstore), counts for nothing.
|
|
16660
|
-
* `from` null reads `latest` alone. Throws when the registry's document cannot be read.
|
|
16661
|
-
*/
|
|
16662
|
-
async function declaredSince(registry, verify, packageName, from, latest) {
|
|
16663
|
-
const { packument } = await registry(packageName);
|
|
16664
|
-
const declared = (version) => releaseDeclaration(packument.versions?.[version]);
|
|
16665
|
-
const range = from === null ? [] : releasesBetween(packument, from, latest);
|
|
16666
|
-
const deciding = [.../* @__PURE__ */ new Set([...range, latest])].filter((version) => declared(version).needsDecision);
|
|
16667
|
-
const verified = /* @__PURE__ */ new Map();
|
|
16668
|
-
const check = async (version) => {
|
|
16669
|
-
const known = verified.get(version);
|
|
16670
|
-
if (known !== void 0) return known;
|
|
16671
|
-
const integrity = releaseIntegrity(packument, version);
|
|
16672
|
-
const ok = verify !== null && integrity !== null && await verify({
|
|
16673
|
-
packageName,
|
|
16674
|
-
version,
|
|
16675
|
-
integrity
|
|
16676
|
-
}).then((v) => v.ok);
|
|
16677
|
-
verified.set(version, ok);
|
|
16678
|
-
return ok;
|
|
16679
|
-
};
|
|
16680
|
-
const decisions = [];
|
|
16681
|
-
for (const version of deciding) decisions.push({
|
|
16682
|
-
version,
|
|
16683
|
-
verified: await check(version)
|
|
16684
|
-
});
|
|
16685
|
-
return {
|
|
16686
|
-
security: declared(latest).security && await check(latest),
|
|
16687
|
-
needsDecision: decisions.some((decision) => decision.verified),
|
|
16688
|
-
deciding: decisions
|
|
16689
|
-
};
|
|
16690
|
-
}
|
|
16691
|
-
/**
|
|
16692
16697
|
* Registering, pruning and listing every declared channel or service MCP server, from the core.
|
|
16693
16698
|
*
|
|
16694
16699
|
* `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
|
|
@@ -17006,6 +17011,7 @@ function serverInstallChange(core, env, request, own, deps = {}) {
|
|
|
17006
17011
|
});
|
|
17007
17012
|
const unverifiedOwn = fetches && deps.verifyRelease === void 0 && running !== void 0 && running.packageName === product.packageName && running.version === version;
|
|
17008
17013
|
const verified = fetches && !unverifiedOwn || checksDisk ? (await verifyReleaseSet(from, /* @__PURE__ */ new Map([[product.packageName, version]]), "install", from.registry === null || from.verify === null ? verifyInCore() : void 0)).get(product.packageName) : void 0;
|
|
17014
|
+
if (request.print !== true && launcher === "npx" && target.writes && deps.verifyRelease !== void 0) await verifyReleaseSet(from, /* @__PURE__ */ new Map([[facts.npxPackage, version]]), "install");
|
|
17009
17015
|
planned = {
|
|
17010
17016
|
product,
|
|
17011
17017
|
install: plannedInstall(preflight),
|
|
@@ -17185,7 +17191,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
17185
17191
|
};
|
|
17186
17192
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
17187
17193
|
packageName: serverFacts(request.channel).packageName,
|
|
17188
|
-
version: "0.
|
|
17194
|
+
version: "0.26.1"
|
|
17189
17195
|
};
|
|
17190
17196
|
const product = {
|
|
17191
17197
|
packageName,
|
|
@@ -17339,6 +17345,599 @@ Object.freeze([
|
|
|
17339
17345
|
"off"
|
|
17340
17346
|
]);
|
|
17341
17347
|
Object.keys(STANDING_LIMITS);
|
|
17348
|
+
const UNPACKED_MAX_BYTES = 134217728;
|
|
17349
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
17350
|
+
const REDIRECTS = /* @__PURE__ */ new Set([
|
|
17351
|
+
301,
|
|
17352
|
+
302,
|
|
17353
|
+
303,
|
|
17354
|
+
307,
|
|
17355
|
+
308
|
|
17356
|
+
]);
|
|
17357
|
+
const BLOCK = 512;
|
|
17358
|
+
/** Where npm's own registry serves a release's tarball: `https://registry.npmjs.org/<name>/-/<file>-<version>.tgz`. */
|
|
17359
|
+
function canonicalTarballUrl(packageName, version) {
|
|
17360
|
+
return `${NPM_REGISTRY}${packageName}/-/${packageName.split("/").at(-1) ?? packageName}-${version}.tgz`;
|
|
17361
|
+
}
|
|
17362
|
+
/** `value` as an absolute `http:` or `https:` address (against `base` when given), without a user or password. */
|
|
17363
|
+
function httpAddress(value, base) {
|
|
17364
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
17365
|
+
let url;
|
|
17366
|
+
try {
|
|
17367
|
+
url = base === void 0 ? new URL(value) : new URL(value, base);
|
|
17368
|
+
} catch {
|
|
17369
|
+
return null;
|
|
17370
|
+
}
|
|
17371
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
17372
|
+
url.username = "";
|
|
17373
|
+
url.password = "";
|
|
17374
|
+
return url.href;
|
|
17375
|
+
}
|
|
17376
|
+
/**
|
|
17377
|
+
* `dist.tarball` of `version` when it is an absolute http(s) URL; otherwise null. A user and password in it are left
|
|
17378
|
+
* out: no credential is sent with a tarball request, so a mirror that needs one falls back to npm's address.
|
|
17379
|
+
*/
|
|
17380
|
+
function tarballLocation(packument, version) {
|
|
17381
|
+
return httpAddress(packument.versions?.[version]?.dist?.tarball);
|
|
17382
|
+
}
|
|
17383
|
+
/**
|
|
17384
|
+
* The fetch a tarball is asked with, unless a caller hands in another: Node's own, asking for the bytes as they are
|
|
17385
|
+
* stored (`accept-encoding: identity` — otherwise it asks for a compressed transfer and decodes it), with no
|
|
17386
|
+
* credential and no cookie.
|
|
17387
|
+
*/
|
|
17388
|
+
const fetchTarball = (url, init) => fetch(url, {
|
|
17389
|
+
...init,
|
|
17390
|
+
headers: { "accept-encoding": "identity" },
|
|
17391
|
+
credentials: "omit"
|
|
17392
|
+
});
|
|
17393
|
+
function discard(response) {
|
|
17394
|
+
response.body?.cancel().catch(() => void 0);
|
|
17395
|
+
}
|
|
17396
|
+
/**
|
|
17397
|
+
* One location: its redirects followed by hand, at most five, each to http(s); its body counted as it arrives; ended
|
|
17398
|
+
* by its own time, or by `deadline` — the caller's — whichever is first.
|
|
17399
|
+
*/
|
|
17400
|
+
async function fetchOne(ask, url, timeoutMs, deadline) {
|
|
17401
|
+
const own = AbortSignal.timeout(timeoutMs);
|
|
17402
|
+
const signal = deadline === void 0 ? own : AbortSignal.any([own, deadline]);
|
|
17403
|
+
let current = url;
|
|
17404
|
+
try {
|
|
17405
|
+
for (let redirects = 0;; redirects += 1) {
|
|
17406
|
+
const response = await ask(current, {
|
|
17407
|
+
redirect: "manual",
|
|
17408
|
+
signal
|
|
17409
|
+
});
|
|
17410
|
+
if (REDIRECTS.has(response.status)) {
|
|
17411
|
+
discard(response);
|
|
17412
|
+
const next = httpAddress(response.headers.get("location"), current);
|
|
17413
|
+
if (next === null || redirects >= 5) return {
|
|
17414
|
+
ok: false,
|
|
17415
|
+
problem: "download"
|
|
17416
|
+
};
|
|
17417
|
+
current = next;
|
|
17418
|
+
continue;
|
|
17419
|
+
}
|
|
17420
|
+
if (!response.ok) {
|
|
17421
|
+
discard(response);
|
|
17422
|
+
return {
|
|
17423
|
+
ok: false,
|
|
17424
|
+
problem: "download"
|
|
17425
|
+
};
|
|
17426
|
+
}
|
|
17427
|
+
const declared = Number(response.headers.get("content-length") ?? NaN);
|
|
17428
|
+
if (Number.isFinite(declared) && declared > 33554432) {
|
|
17429
|
+
discard(response);
|
|
17430
|
+
return {
|
|
17431
|
+
ok: false,
|
|
17432
|
+
problem: "size"
|
|
17433
|
+
};
|
|
17434
|
+
}
|
|
17435
|
+
if (response.body === null) return {
|
|
17436
|
+
ok: true,
|
|
17437
|
+
bytes: Buffer.alloc(0)
|
|
17438
|
+
};
|
|
17439
|
+
const reader = response.body.getReader();
|
|
17440
|
+
const chunks = [];
|
|
17441
|
+
let total = 0;
|
|
17442
|
+
for (;;) {
|
|
17443
|
+
const { done, value } = await reader.read();
|
|
17444
|
+
if (done) break;
|
|
17445
|
+
total += value.byteLength;
|
|
17446
|
+
if (total > 33554432) {
|
|
17447
|
+
reader.cancel().catch(() => void 0);
|
|
17448
|
+
return {
|
|
17449
|
+
ok: false,
|
|
17450
|
+
problem: "size"
|
|
17451
|
+
};
|
|
17452
|
+
}
|
|
17453
|
+
chunks.push(Buffer.from(value.buffer, value.byteOffset, value.byteLength));
|
|
17454
|
+
}
|
|
17455
|
+
return {
|
|
17456
|
+
ok: true,
|
|
17457
|
+
bytes: Buffer.concat(chunks, total)
|
|
17458
|
+
};
|
|
17459
|
+
}
|
|
17460
|
+
} catch {
|
|
17461
|
+
return {
|
|
17462
|
+
ok: false,
|
|
17463
|
+
problem: "download"
|
|
17464
|
+
};
|
|
17465
|
+
}
|
|
17466
|
+
}
|
|
17467
|
+
/** Of two failed downloads, the one that says more: bytes that came and were not the release's, then too many. */
|
|
17468
|
+
const RANK = {
|
|
17469
|
+
download: 0,
|
|
17470
|
+
size: 1,
|
|
17471
|
+
digest: 2
|
|
17472
|
+
};
|
|
17473
|
+
/**
|
|
17474
|
+
* The release's tarball: from `location`, then — if that fails in any way — once from npm's canonical address, which
|
|
17475
|
+
* is never asked twice. Kept only when its sha512 is the verified one; otherwise the problem that says the most of the
|
|
17476
|
+
* two tries. A caller's `signal` ends it: what is being fetched is cut off, and nothing more is asked.
|
|
17477
|
+
*/
|
|
17478
|
+
async function downloadVerifiedTarball(release, location, options = {}) {
|
|
17479
|
+
const ask = options.fetch ?? fetchTarball;
|
|
17480
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
17481
|
+
const want = release.sha512.toLowerCase();
|
|
17482
|
+
const canonical = canonicalTarballUrl(release.packageName, release.version);
|
|
17483
|
+
const first = location === null ? null : httpAddress(location);
|
|
17484
|
+
let problem = "download";
|
|
17485
|
+
for (const place of first === null || first === canonical ? [canonical] : [first, canonical]) {
|
|
17486
|
+
if (options.signal?.aborted) break;
|
|
17487
|
+
const fetched = await fetchOne(ask, place, timeoutMs, options.signal);
|
|
17488
|
+
let failed;
|
|
17489
|
+
if (fetched.ok) {
|
|
17490
|
+
if (createHash("sha512").update(fetched.bytes).digest("hex") === want) return fetched;
|
|
17491
|
+
failed = "digest";
|
|
17492
|
+
} else failed = fetched.problem;
|
|
17493
|
+
if (RANK[failed] > RANK[problem]) problem = failed;
|
|
17494
|
+
}
|
|
17495
|
+
return {
|
|
17496
|
+
ok: false,
|
|
17497
|
+
problem
|
|
17498
|
+
};
|
|
17499
|
+
}
|
|
17500
|
+
/** A header field's text: its bytes up to the first NUL. */
|
|
17501
|
+
function fieldText(header, at, width) {
|
|
17502
|
+
const raw = header.subarray(at, at + width);
|
|
17503
|
+
const end = raw.indexOf(0);
|
|
17504
|
+
return raw.subarray(0, end === -1 ? width : end).toString("utf8");
|
|
17505
|
+
}
|
|
17506
|
+
/** A header's octal number, spaces around it allowed; null for base-256 (the high bit set) or anything not octal. */
|
|
17507
|
+
function octalField(header, at, width) {
|
|
17508
|
+
if ((header[at] & 128) !== 0) return null;
|
|
17509
|
+
const digits = fieldText(header, at, width).trim();
|
|
17510
|
+
if (digits === "") return 0;
|
|
17511
|
+
if (!/^[0-7]+$/.test(digits)) return null;
|
|
17512
|
+
const value = Number.parseInt(digits, 8);
|
|
17513
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
17514
|
+
}
|
|
17515
|
+
/** The checksum ustar stores at 148: the header's byte sum, its own eight bytes read as spaces. */
|
|
17516
|
+
function checksumHolds(header) {
|
|
17517
|
+
const stored = octalField(header, 148, 8);
|
|
17518
|
+
if (stored === null) return false;
|
|
17519
|
+
let sum = 0;
|
|
17520
|
+
for (let at = 0; at < BLOCK; at += 1) sum += at >= 148 && at < 156 ? 32 : header[at];
|
|
17521
|
+
return sum === stored;
|
|
17522
|
+
}
|
|
17523
|
+
/** A pax extended header's records, `<length> <key>=<value>\n` each; null when one is not that. */
|
|
17524
|
+
function paxRecords(data) {
|
|
17525
|
+
const records = /* @__PURE__ */ new Map();
|
|
17526
|
+
let at = 0;
|
|
17527
|
+
while (at < data.length) {
|
|
17528
|
+
if (data.subarray(at).every((byte) => byte === 0)) break;
|
|
17529
|
+
const space = data.indexOf(32, at);
|
|
17530
|
+
if (space === -1) return null;
|
|
17531
|
+
const length = data.subarray(at, space).toString("latin1");
|
|
17532
|
+
if (!/^[1-9]\d*$/.test(length)) return null;
|
|
17533
|
+
const end = at + Number(length);
|
|
17534
|
+
if (end > data.length || end <= space + 1 || data[end - 1] !== 10) return null;
|
|
17535
|
+
const record = data.subarray(space + 1, end - 1).toString("utf8");
|
|
17536
|
+
const equals = record.indexOf("=");
|
|
17537
|
+
if (equals <= 0) return null;
|
|
17538
|
+
records.set(record.slice(0, equals), record.slice(equals + 1));
|
|
17539
|
+
at = end;
|
|
17540
|
+
}
|
|
17541
|
+
return records;
|
|
17542
|
+
}
|
|
17543
|
+
/**
|
|
17544
|
+
* An entry's path below `package/`, POSIX, with any leading `./` gone and empty or `.` segments dropped: `''` for the
|
|
17545
|
+
* folder itself. Null for anything outside `package/`, a `..` segment, or a backslash, which no npm pack writes and
|
|
17546
|
+
* which some file systems read as a separator.
|
|
17547
|
+
*/
|
|
17548
|
+
function belowPackage(name) {
|
|
17549
|
+
let rest = name;
|
|
17550
|
+
while (rest.startsWith("./")) rest = rest.slice(2);
|
|
17551
|
+
if (rest.includes("\\")) return null;
|
|
17552
|
+
const parts = rest.split("/");
|
|
17553
|
+
if (parts[0] !== "package" || parts.includes("..")) return null;
|
|
17554
|
+
return parts.slice(1).filter((part) => part !== "" && part !== ".").join("/");
|
|
17555
|
+
}
|
|
17556
|
+
function entryType(flag) {
|
|
17557
|
+
if (flag === "0" || flag === "\0") return "file";
|
|
17558
|
+
if (flag === "1" || flag === "2") return "link";
|
|
17559
|
+
if (flag === "5") return "directory";
|
|
17560
|
+
return "other";
|
|
17561
|
+
}
|
|
17562
|
+
/**
|
|
17563
|
+
* Every entry of a release's archive, in order: gunzipped with `node:zlib`, stopping past 128 MiB, then walked as
|
|
17564
|
+
* ustar — 512-byte headers, each checksum checked, to the first all-zero block. A pax `x` header's `path` and `size`,
|
|
17565
|
+
* and a GNU `L` long name, apply to the next entry. `package/` itself is not listed.
|
|
17566
|
+
*/
|
|
17567
|
+
function tarballEntries(gz) {
|
|
17568
|
+
let tar;
|
|
17569
|
+
try {
|
|
17570
|
+
tar = gunzipSync(gz, { maxOutputLength: UNPACKED_MAX_BYTES });
|
|
17571
|
+
} catch {
|
|
17572
|
+
return {
|
|
17573
|
+
ok: false,
|
|
17574
|
+
problem: "gzip"
|
|
17575
|
+
};
|
|
17576
|
+
}
|
|
17577
|
+
const broken = {
|
|
17578
|
+
ok: false,
|
|
17579
|
+
problem: "archive"
|
|
17580
|
+
};
|
|
17581
|
+
const entries = [];
|
|
17582
|
+
let pax = {};
|
|
17583
|
+
let longName;
|
|
17584
|
+
for (let offset = 0; offset + BLOCK <= tar.length;) {
|
|
17585
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
17586
|
+
if (header.every((byte) => byte === 0)) break;
|
|
17587
|
+
if (!checksumHolds(header)) return broken;
|
|
17588
|
+
const ownSize = octalField(header, 124, 12);
|
|
17589
|
+
if (ownSize === null) return broken;
|
|
17590
|
+
const flag = String.fromCharCode(header[156]);
|
|
17591
|
+
const size = flag === "x" || flag === "L" || flag === "K" || flag === "g" ? ownSize : pax.size ?? ownSize;
|
|
17592
|
+
const start = offset + BLOCK;
|
|
17593
|
+
if (start + size > tar.length) return broken;
|
|
17594
|
+
const data = tar.subarray(start, start + size);
|
|
17595
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
17596
|
+
if (flag === "g") return broken;
|
|
17597
|
+
if (flag === "x") {
|
|
17598
|
+
const records = paxRecords(data);
|
|
17599
|
+
if (records === null) return broken;
|
|
17600
|
+
const path = records.get("path");
|
|
17601
|
+
const paxSize = records.get("size");
|
|
17602
|
+
if (paxSize !== void 0 && !/^\d+$/.test(paxSize)) return broken;
|
|
17603
|
+
pax = {
|
|
17604
|
+
...pax,
|
|
17605
|
+
...path !== void 0 ? { path } : {},
|
|
17606
|
+
...paxSize !== void 0 ? { size: Number(paxSize) } : {}
|
|
17607
|
+
};
|
|
17608
|
+
if (pax.size !== void 0 && !Number.isSafeInteger(pax.size)) return broken;
|
|
17609
|
+
continue;
|
|
17610
|
+
}
|
|
17611
|
+
if (flag === "L") {
|
|
17612
|
+
const end = data.indexOf(0);
|
|
17613
|
+
longName = data.subarray(0, end === -1 ? data.length : end).toString("utf8");
|
|
17614
|
+
continue;
|
|
17615
|
+
}
|
|
17616
|
+
if (flag === "K") continue;
|
|
17617
|
+
const prefix = fieldText(header, 257, 6) === "ustar" ? fieldText(header, 345, 155) : "";
|
|
17618
|
+
const own = fieldText(header, 0, 100);
|
|
17619
|
+
const name = pax.path ?? longName ?? (prefix === "" ? own : `${prefix}/${own}`);
|
|
17620
|
+
pax = {};
|
|
17621
|
+
longName = void 0;
|
|
17622
|
+
const path = belowPackage(name);
|
|
17623
|
+
const type = entryType(flag);
|
|
17624
|
+
if (path === null) return broken;
|
|
17625
|
+
if (path === "") {
|
|
17626
|
+
if (type === "directory") continue;
|
|
17627
|
+
return broken;
|
|
17628
|
+
}
|
|
17629
|
+
entries.push({
|
|
17630
|
+
path,
|
|
17631
|
+
type,
|
|
17632
|
+
data
|
|
17633
|
+
});
|
|
17634
|
+
}
|
|
17635
|
+
return {
|
|
17636
|
+
ok: true,
|
|
17637
|
+
entries
|
|
17638
|
+
};
|
|
17639
|
+
}
|
|
17640
|
+
/**
|
|
17641
|
+
* The release's `package/package.json`, as an object: exactly one entry by that name (in any case), a regular file of
|
|
17642
|
+
* at most 1 MiB, holding a JSON object. Anything else is `manifest`; an archive that cannot be walked, `archive`.
|
|
17643
|
+
*/
|
|
17644
|
+
function tarballManifest(gz) {
|
|
17645
|
+
const read = tarballEntries(gz);
|
|
17646
|
+
if (!read.ok) return read;
|
|
17647
|
+
const none = {
|
|
17648
|
+
ok: false,
|
|
17649
|
+
problem: "manifest"
|
|
17650
|
+
};
|
|
17651
|
+
const named = read.entries.filter((entry) => entry.path.toLowerCase() === "package.json");
|
|
17652
|
+
const [entry] = named;
|
|
17653
|
+
if (named.length !== 1 || entry === void 0) return none;
|
|
17654
|
+
if (entry.path !== "package.json" || entry.type !== "file" || entry.data.length > 1048576) return none;
|
|
17655
|
+
let manifest;
|
|
17656
|
+
try {
|
|
17657
|
+
manifest = JSON.parse(entry.data.toString("utf8").replace(/^/, ""));
|
|
17658
|
+
} catch {
|
|
17659
|
+
return none;
|
|
17660
|
+
}
|
|
17661
|
+
if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) return none;
|
|
17662
|
+
return {
|
|
17663
|
+
ok: true,
|
|
17664
|
+
manifest
|
|
17665
|
+
};
|
|
17666
|
+
}
|
|
17667
|
+
const REMOVED_GLOBALS_FILE = "removed-globals.json";
|
|
17668
|
+
function isTime(value) {
|
|
17669
|
+
if (typeof value !== "string") return false;
|
|
17670
|
+
const at = new Date(value);
|
|
17671
|
+
return !Number.isNaN(at.getTime()) && at.toISOString() === value;
|
|
17672
|
+
}
|
|
17673
|
+
/**
|
|
17674
|
+
* The globals recorded for the next update to reinstall, each once: none when there is no record or it cannot be
|
|
17675
|
+
* read. Only a name of this suite and a plain release are read from it.
|
|
17676
|
+
*/
|
|
17677
|
+
async function readRemovedGlobals(stateDir) {
|
|
17678
|
+
let parsed;
|
|
17679
|
+
try {
|
|
17680
|
+
parsed = JSON.parse(await readFile(join(stateDir, REMOVED_GLOBALS_FILE), "utf8"));
|
|
17681
|
+
} catch {
|
|
17682
|
+
return [];
|
|
17683
|
+
}
|
|
17684
|
+
const listed = typeof parsed === "object" && parsed !== null ? parsed.removed : void 0;
|
|
17685
|
+
if (!Array.isArray(listed)) return [];
|
|
17686
|
+
const removed = [];
|
|
17687
|
+
for (const item of listed) {
|
|
17688
|
+
if (typeof item !== "object" || item === null) continue;
|
|
17689
|
+
const { package: name, version, removedAt } = item;
|
|
17690
|
+
if (typeof name !== "string" || !SUITE_NAME.test(name) || typeof version !== "string") continue;
|
|
17691
|
+
if (!isPlainVersion(version) || !isTime(removedAt)) continue;
|
|
17692
|
+
if (removed.some((one) => one.package === name)) continue;
|
|
17693
|
+
removed.push({
|
|
17694
|
+
package: name,
|
|
17695
|
+
version,
|
|
17696
|
+
removedAt
|
|
17697
|
+
});
|
|
17698
|
+
}
|
|
17699
|
+
return removed;
|
|
17700
|
+
}
|
|
17701
|
+
/** A name of this suite, as npm writes one: only then is it said in a detail. */
|
|
17702
|
+
const SUITE_NAME = /^@agentcomms\/[a-z0-9][a-z0-9._-]{0,213}$/;
|
|
17703
|
+
/**
|
|
17704
|
+
* What a release declares — a security fix, a decision it needs from the person — read from its verified tarball, and
|
|
17705
|
+
* never from the registry (design 2026-10-22 P1). The registry's copy of a manifest is the registry's word: a mirror
|
|
17706
|
+
* could add or remove either flag on a genuine release. The tarball whose sha512 is the verified provenance digest is
|
|
17707
|
+
* the release's own.
|
|
17708
|
+
*
|
|
17709
|
+
* What was read is kept per integrity, content-free — the version, the integrity, the two flags and the release a
|
|
17710
|
+
* needed decision is carried from (design 2026-10-22 §8.1), never a word of the manifest — in
|
|
17711
|
+
* `<state>/release-declarations.json`, so neither the daily check nor the background run downloads a release it read.
|
|
17712
|
+
* A declaration that cannot be read is unknown: it is never cached, so the next check or run tries it again, and the
|
|
17713
|
+
* caller fails closed. The cache saves downloads, not verification: the caller verifies the release first, every time.
|
|
17714
|
+
*/
|
|
17715
|
+
const DECLARATIONS_FILE = "release-declarations.json";
|
|
17716
|
+
const LOCK_FILE = ".release-declarations.lock";
|
|
17717
|
+
const INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
|
|
17718
|
+
function declarationsPath(stateDir) {
|
|
17719
|
+
return join(stateDir, DECLARATIONS_FILE);
|
|
17720
|
+
}
|
|
17721
|
+
/**
|
|
17722
|
+
* A record as the cache keeps it, or null for anything else: only these five keys, each of its type. One without
|
|
17723
|
+
* `needsDecisionSince`, as a build before it was carried wrote, is no record: it costs a download, never a decision.
|
|
17724
|
+
*/
|
|
17725
|
+
function recordOf(value) {
|
|
17726
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17727
|
+
const { version, integrity, security, needsDecision, needsDecisionSince } = value;
|
|
17728
|
+
if (!isVersion(version) || typeof integrity !== "string" || !INTEGRITY.test(integrity)) return null;
|
|
17729
|
+
if (typeof security !== "boolean" || typeof needsDecision !== "boolean") return null;
|
|
17730
|
+
if (needsDecisionSince !== null && !isPlainVersion(needsDecisionSince)) return null;
|
|
17731
|
+
return {
|
|
17732
|
+
version,
|
|
17733
|
+
integrity,
|
|
17734
|
+
security,
|
|
17735
|
+
needsDecision,
|
|
17736
|
+
needsDecisionSince
|
|
17737
|
+
};
|
|
17738
|
+
}
|
|
17739
|
+
/** The cache's records; a file that is missing or cannot be read counts as empty. */
|
|
17740
|
+
async function readCache(stateDir) {
|
|
17741
|
+
try {
|
|
17742
|
+
const listed = JSON.parse(await readFile(declarationsPath(stateDir), "utf8"))?.declarations;
|
|
17743
|
+
if (!Array.isArray(listed)) return [];
|
|
17744
|
+
return listed.map(recordOf).filter((record) => record !== null);
|
|
17745
|
+
} catch {
|
|
17746
|
+
return [];
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
/**
|
|
17750
|
+
* Adds one record, under the cache's lock, to the file as it is then — so two processes reading at once each keep
|
|
17751
|
+
* the other's — keeping the newest 64. Best effort: a write that fails changes no answer, and costs a download later.
|
|
17752
|
+
*/
|
|
17753
|
+
async function remember(stateDir, record) {
|
|
17754
|
+
try {
|
|
17755
|
+
await withFileLock(join(stateDir, LOCK_FILE), async () => {
|
|
17756
|
+
const declarations = [...(await readCache(stateDir)).filter((one) => one.integrity !== record.integrity || one.version !== record.version), record].slice(-64);
|
|
17757
|
+
await writeFileAtomic(declarationsPath(stateDir), `${JSON.stringify({ declarations }, null, 2)}\n`);
|
|
17758
|
+
});
|
|
17759
|
+
} catch {}
|
|
17760
|
+
}
|
|
17761
|
+
/**
|
|
17762
|
+
* What a verified release declares: from the cache, by its integrity and version, or else from its tarball — the bytes
|
|
17763
|
+
* whose sha512 is `provenance.sha512`, from `location` or npm's canonical address — whose `package/package.json` must
|
|
17764
|
+
* name the release itself. Unknown, with the problem, when it cannot be read, or when `signal` ends the read first. A
|
|
17765
|
+
* known declaration is cached unless `writeCache` is false (a dry run); an unknown one never is.
|
|
17766
|
+
*/
|
|
17767
|
+
async function readVerifiedDeclaration(provenance, location, options) {
|
|
17768
|
+
const { version } = provenance;
|
|
17769
|
+
const integrity = `sha512-${Buffer.from(provenance.sha512, "hex").toString("base64")}`;
|
|
17770
|
+
const hit = (await readCache(options.stateDir)).find((record) => record.integrity === integrity && record.version === version);
|
|
17771
|
+
if (hit !== void 0) {
|
|
17772
|
+
const { security, needsDecision, needsDecisionSince } = hit;
|
|
17773
|
+
return {
|
|
17774
|
+
known: true,
|
|
17775
|
+
version,
|
|
17776
|
+
security,
|
|
17777
|
+
needsDecision,
|
|
17778
|
+
needsDecisionSince,
|
|
17779
|
+
from: "cache"
|
|
17780
|
+
};
|
|
17781
|
+
}
|
|
17782
|
+
const download = await downloadVerifiedTarball({
|
|
17783
|
+
packageName: provenance.package,
|
|
17784
|
+
version,
|
|
17785
|
+
sha512: provenance.sha512
|
|
17786
|
+
}, location, {
|
|
17787
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
17788
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
17789
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
17790
|
+
});
|
|
17791
|
+
if (!download.ok) return {
|
|
17792
|
+
known: false,
|
|
17793
|
+
version,
|
|
17794
|
+
problem: download.problem
|
|
17795
|
+
};
|
|
17796
|
+
const read = tarballManifest(download.bytes);
|
|
17797
|
+
if (!read.ok) return {
|
|
17798
|
+
known: false,
|
|
17799
|
+
version,
|
|
17800
|
+
problem: read.problem
|
|
17801
|
+
};
|
|
17802
|
+
if (read.manifest.name !== provenance.package || read.manifest.version !== version) return {
|
|
17803
|
+
known: false,
|
|
17804
|
+
version,
|
|
17805
|
+
problem: "identity"
|
|
17806
|
+
};
|
|
17807
|
+
const { security, needsDecision, needsDecisionSince } = releaseDeclaration(read.manifest);
|
|
17808
|
+
if (options.writeCache !== false) await remember(options.stateDir, {
|
|
17809
|
+
version,
|
|
17810
|
+
integrity,
|
|
17811
|
+
security,
|
|
17812
|
+
needsDecision,
|
|
17813
|
+
needsDecisionSince
|
|
17814
|
+
});
|
|
17815
|
+
return {
|
|
17816
|
+
known: true,
|
|
17817
|
+
version,
|
|
17818
|
+
security,
|
|
17819
|
+
needsDecision,
|
|
17820
|
+
needsDecisionSince,
|
|
17821
|
+
from: "tarball"
|
|
17822
|
+
};
|
|
17823
|
+
}
|
|
17824
|
+
/**
|
|
17825
|
+
* What the release an update goes to declares, believed only from its verified tarball (design 2026-10-22 P1, P2,
|
|
17826
|
+
* §8.1): for the daily check's record, `update --check`, and the by-hand update's preview.
|
|
17827
|
+
*
|
|
17828
|
+
* Only that one release is read. A needed decision is carried forward from release to release in core's
|
|
17829
|
+
* `agentcommsRelease.needsDecisionSince`, which the release check holds to the changelog, so the release an update goes
|
|
17830
|
+
* to says itself whether a release on the way needed one: which versions the registry lists decides nothing. A registry
|
|
17831
|
+
* or mirror that leaves out the release needing a decision, adds an unverifiable one, or lists five hundred, changes no
|
|
17832
|
+
* fact and costs no request.
|
|
17833
|
+
*
|
|
17834
|
+
* The registry's document is read once, for that release's integrity and where its tarball is — never for what it
|
|
17835
|
+
* declares: a registry or a mirror can add or remove either flag on a genuine release. The release is verified, and
|
|
17836
|
+
* then its declaration read (`readVerifiedDeclaration`) from the bytes whose sha512 its verified provenance signs. One
|
|
17837
|
+
* that does not verify, that the registry gives no integrity for, whose tarball cannot be read, or whose read is cut off
|
|
17838
|
+
* by its deadline is *unknown*, and counts for nothing either way: neither a security fix nor its absence, neither a
|
|
17839
|
+
* decision nor none.
|
|
17840
|
+
*
|
|
17841
|
+
* Only core reads these, since only core carries the verifier: a channel's process reads none (`releaseFactsReader`).
|
|
17842
|
+
* No Sigstore here, and nothing runs when this module is loaded: WhatsApp's bundle, which reaches the module that
|
|
17843
|
+
* imports this one, drops it.
|
|
17844
|
+
*/
|
|
17845
|
+
/**
|
|
17846
|
+
* How long a check gives its target's declaration — its document, its verification and its tarball, from both places —
|
|
17847
|
+
* before it counts it unknown (review B of 0.26.0, finding 2). A check runs under a claim of `UPDATE_CHECK_LEASE_MS`,
|
|
17848
|
+
* two minutes, which also holds the registry's ten seconds for `latest`, `npm ls`'s minute and up to 15 seconds for
|
|
17849
|
+
* the record's lock: 30 seconds beside them still records inside the lease, where a tarball's two tries of a minute
|
|
17850
|
+
* each did not, and the check's finding was dropped as superseded.
|
|
17851
|
+
*/
|
|
17852
|
+
const DECLARATION_DEADLINE_MS = 3e4;
|
|
17853
|
+
/**
|
|
17854
|
+
* What `version` declares, read from its verified tarball, and nothing of any other release. Unknown when the
|
|
17855
|
+
* registry's document cannot be read or gives no integrity for it, when it does not verify, when its tarball cannot be
|
|
17856
|
+
* read, or when `signal` ends the read first. Never throws.
|
|
17857
|
+
*/
|
|
17858
|
+
async function targetDeclaration(sources, packageName, version, options = {}) {
|
|
17859
|
+
const unknown = {
|
|
17860
|
+
known: false,
|
|
17861
|
+
version
|
|
17862
|
+
};
|
|
17863
|
+
try {
|
|
17864
|
+
const { packument } = await sources.registry(packageName);
|
|
17865
|
+
const integrity = releaseIntegrity(packument, version);
|
|
17866
|
+
if (integrity === null || options.signal?.aborted) return unknown;
|
|
17867
|
+
const verdict = await sources.verify({
|
|
17868
|
+
packageName,
|
|
17869
|
+
version,
|
|
17870
|
+
integrity
|
|
17871
|
+
});
|
|
17872
|
+
if (!verdict.ok || options.signal?.aborted) return unknown;
|
|
17873
|
+
const read = await readVerifiedDeclaration(verdict.provenance, tarballLocation(packument, version), {
|
|
17874
|
+
stateDir: sources.stateDir,
|
|
17875
|
+
...sources.tarballs !== void 0 ? { fetch: sources.tarballs } : {},
|
|
17876
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
17877
|
+
});
|
|
17878
|
+
if (!read.known) return unknown;
|
|
17879
|
+
const { security, needsDecision, needsDecisionSince } = read;
|
|
17880
|
+
return {
|
|
17881
|
+
known: true,
|
|
17882
|
+
version,
|
|
17883
|
+
security,
|
|
17884
|
+
needsDecision,
|
|
17885
|
+
needsDecisionSince
|
|
17886
|
+
};
|
|
17887
|
+
} catch {
|
|
17888
|
+
return unknown;
|
|
17889
|
+
}
|
|
17890
|
+
}
|
|
17891
|
+
/**
|
|
17892
|
+
* `read`, given `deadlineMs` at most: unknown once that has passed, and the read cut off then — never a guess of
|
|
17893
|
+
* `false`. The timer is cleared as soon as the read is done.
|
|
17894
|
+
*/
|
|
17895
|
+
async function withinDeadline(read, version, deadlineMs = DECLARATION_DEADLINE_MS) {
|
|
17896
|
+
const controller = new AbortController();
|
|
17897
|
+
let timer;
|
|
17898
|
+
const passed = new Promise((resolve) => {
|
|
17899
|
+
timer = setTimeout(() => {
|
|
17900
|
+
controller.abort();
|
|
17901
|
+
resolve({
|
|
17902
|
+
known: false,
|
|
17903
|
+
version
|
|
17904
|
+
});
|
|
17905
|
+
}, deadlineMs);
|
|
17906
|
+
});
|
|
17907
|
+
try {
|
|
17908
|
+
return await Promise.race([read(controller.signal), passed]);
|
|
17909
|
+
} finally {
|
|
17910
|
+
clearTimeout(timer);
|
|
17911
|
+
}
|
|
17912
|
+
}
|
|
17913
|
+
/**
|
|
17914
|
+
* What the release declares for a machine whose oldest release is `from` (design 2026-10-22 §8.1): its own security
|
|
17915
|
+
* flag, and a decision when it declares one or carries one after `from` (`decisionNeeded`).
|
|
17916
|
+
*/
|
|
17917
|
+
function declaredFor(declared, from) {
|
|
17918
|
+
if (!declared.known) return {
|
|
17919
|
+
security: null,
|
|
17920
|
+
needsDecision: null,
|
|
17921
|
+
deciding: null,
|
|
17922
|
+
unread: true
|
|
17923
|
+
};
|
|
17924
|
+
const deciding = decisionNeeded(declared, from);
|
|
17925
|
+
return {
|
|
17926
|
+
security: declared.security,
|
|
17927
|
+
needsDecision: deciding !== null,
|
|
17928
|
+
deciding,
|
|
17929
|
+
unread: false
|
|
17930
|
+
};
|
|
17931
|
+
}
|
|
17932
|
+
/** The record's facts for `version`: null unless its declaration was read. */
|
|
17933
|
+
function factsOf(version, declared) {
|
|
17934
|
+
const { security, needsDecision } = declared;
|
|
17935
|
+
return security === null || needsDecision === null ? null : {
|
|
17936
|
+
version,
|
|
17937
|
+
security,
|
|
17938
|
+
needsDecision
|
|
17939
|
+
};
|
|
17940
|
+
}
|
|
17342
17941
|
/**
|
|
17343
17942
|
* Bringing a machine to the latest release, from a terminal (`agentcomms update`) or a chat (`comms_update`).
|
|
17344
17943
|
*
|
|
@@ -17507,6 +18106,7 @@ async function inspect(core, env, deps) {
|
|
|
17507
18106
|
package: server.args.some((arg) => arg.startsWith(`${facts.npxPackage}@`)) ? facts.npxPackage : facts.packageName
|
|
17508
18107
|
};
|
|
17509
18108
|
});
|
|
18109
|
+
const removedGlobals = (await readRemovedGlobals(core.paths.stateDir)).filter((one) => PUBLISHED_NAMES.includes(one.package));
|
|
17510
18110
|
let installed = {};
|
|
17511
18111
|
try {
|
|
17512
18112
|
const listed = await (deps.globalPackages ?? (() => npmGlobalPackages(env, PUBLISHED_NAMES)))();
|
|
@@ -17524,6 +18124,7 @@ async function inspect(core, env, deps) {
|
|
|
17524
18124
|
wanted.add(entry.package);
|
|
17525
18125
|
}
|
|
17526
18126
|
for (const name of Object.keys(installed)) wanted.add(name);
|
|
18127
|
+
for (const removed of removedGlobals) wanted.add(removed.package);
|
|
17527
18128
|
const latest = await latestReleases([...wanted].sort(), deps.latestVersion ?? ((name) => npmLatestVersion(name, { env })));
|
|
17528
18129
|
let config = null;
|
|
17529
18130
|
try {
|
|
@@ -17625,16 +18226,19 @@ async function inspect(core, env, deps) {
|
|
|
17625
18226
|
}
|
|
17626
18227
|
const globals = [];
|
|
17627
18228
|
for (const name of PUBLISHED_NAMES) {
|
|
17628
|
-
const
|
|
18229
|
+
const removed = removedGlobals.find((one) => one.package === name);
|
|
18230
|
+
const version = installed[name] ?? removed?.version;
|
|
17629
18231
|
const newestRelease = latest[name];
|
|
17630
18232
|
if (version === void 0 || newestRelease === void 0) continue;
|
|
18233
|
+
const reinstall = removed !== void 0 && !isBehind(version, newestRelease);
|
|
17631
18234
|
const item = {
|
|
17632
18235
|
kind: "global",
|
|
17633
18236
|
package: name,
|
|
17634
18237
|
version,
|
|
17635
|
-
latest: newestRelease
|
|
18238
|
+
latest: newestRelease,
|
|
18239
|
+
...reinstall ? { reinstall: true } : {}
|
|
17636
18240
|
};
|
|
17637
|
-
if (isBehind(version, newestRelease)) {
|
|
18241
|
+
if (reinstall || isBehind(version, newestRelease)) {
|
|
17638
18242
|
behind.push(item);
|
|
17639
18243
|
globals.push(item);
|
|
17640
18244
|
} else upToDate.push(item);
|
|
@@ -17669,54 +18273,87 @@ async function inspect(core, env, deps) {
|
|
|
17669
18273
|
*/
|
|
17670
18274
|
async function updateCheck(core, env, deps = {}, claim) {
|
|
17671
18275
|
const { report } = await inspect(core, env, deps);
|
|
17672
|
-
await recordFound(core, env, report, deps, null, claim, await releaseOf(report, env, deps));
|
|
18276
|
+
await recordFound(core, env, report, deps, null, claim, await releaseOf(core, report, env, deps));
|
|
17673
18277
|
return report;
|
|
17674
18278
|
}
|
|
17675
18279
|
/**
|
|
17676
|
-
* What the latest release's core manifest declares, for the daily check's file
|
|
17677
|
-
*
|
|
18280
|
+
* What the latest release's core manifest declares, for the daily check's file, as its verified tarball says (design
|
|
18281
|
+
* 2026-10-22 P1, §8.1): the facts when it was read; null when this check read it and it could not be read; and
|
|
18282
|
+
* undefined where this process reads none — a channel's, with no verifier, or a test's stand-in registry (P2). For
|
|
18283
|
+
* null and undefined alike the record keeps what core's verified check found of the same version, and otherwise makes
|
|
18284
|
+
* no claim. Never a reason for the check to fail.
|
|
17678
18285
|
*/
|
|
17679
|
-
async function releaseOf(report, env, deps) {
|
|
18286
|
+
async function releaseOf(core, report, env, deps) {
|
|
17680
18287
|
const latest = report.latest[CORE_PACKAGE$1];
|
|
17681
|
-
if (latest === void 0 || !isPlainVersion(latest)) return
|
|
17682
|
-
const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps);
|
|
17683
|
-
if (read === null) return
|
|
18288
|
+
if (latest === void 0 || !isPlainVersion(latest)) return void 0;
|
|
18289
|
+
const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps, core.paths.stateDir);
|
|
18290
|
+
if (read === null) return void 0;
|
|
17684
18291
|
try {
|
|
17685
|
-
|
|
18292
|
+
const facts = await read(CORE_PACKAGE$1, latest, oldestHere(report));
|
|
18293
|
+
return facts === null ? null : {
|
|
17686
18294
|
version: latest,
|
|
17687
|
-
|
|
18295
|
+
security: facts.security,
|
|
18296
|
+
needsDecision: facts.needsDecision
|
|
17688
18297
|
};
|
|
17689
18298
|
} catch {
|
|
17690
18299
|
return null;
|
|
17691
18300
|
}
|
|
17692
18301
|
}
|
|
17693
18302
|
/**
|
|
17694
|
-
* The oldest release this machine runs, as the report saw it — what
|
|
17695
|
-
* answering, and every registration, runtime and global package behind.
|
|
18303
|
+
* The oldest release this machine runs, as the report saw it — what a decision the latest carries is counted from: the
|
|
18304
|
+
* core answering, and every registration, runtime and global package behind.
|
|
17696
18305
|
*/
|
|
17697
18306
|
function oldestHere(report) {
|
|
17698
18307
|
return oldestVersion([report.core, ...report.behind.map((item) => item.version)]);
|
|
17699
18308
|
}
|
|
17700
18309
|
/**
|
|
17701
|
-
* The daily check's and `update --check`'s reader of what
|
|
17702
|
-
*
|
|
17703
|
-
*
|
|
17704
|
-
*
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
18310
|
+
* The daily check's and `update --check`'s reader of what a release declares (design 2026-10-22 P1, §8.1): the release
|
|
18311
|
+
* verified, then its declaration read from its tarball and cached in `stateDir`; its facts for each machine's `from`,
|
|
18312
|
+
* or null when it could not be read (`factsOf`). Nothing of any other release is read: a needed decision is carried in
|
|
18313
|
+
* the release itself.
|
|
18314
|
+
*
|
|
18315
|
+
* Each release is read once however often it is asked — the check asks once for the core answering, and again for the
|
|
18316
|
+
* oldest release it finds here — and known or unknown, the answer stands for the reader's life: an unreadable tarball
|
|
18317
|
+
* is asked for at most twice in a check, at its location and at npm's own address (review B of 0.26.0, finding 2).
|
|
18318
|
+
* And the read is given `deadlineMs` at most (`DECLARATION_DEADLINE_MS` unless a test gives another), and stops at
|
|
18319
|
+
* `deadline` — a time, by `Date.now()` — when that comes first: the day's check passes its claim's time plus
|
|
18320
|
+
* `DECLARATION_DEADLINE_MS`, well inside the claim, however late its ask began. After it the read is unknown — never
|
|
18321
|
+
* false — and cut off, and one not started by then is not started at all, so the check records what it found while
|
|
18322
|
+
* its claim is still its own.
|
|
18323
|
+
*
|
|
18324
|
+
* The registry handed in, or the one npm reads — but only beside the registry's own `latest`: a test with a stand-in
|
|
18325
|
+
* `latestVersion` hands in `releaseFacts` itself, or none is read. And only with a verifier (P2): a channel's process
|
|
18326
|
+
* carries no Sigstore, so it reads nothing a release declares — no document, no tarball — and its check keeps what
|
|
18327
|
+
* core's verified check recorded.
|
|
18328
|
+
*/
|
|
18329
|
+
function releaseFactsReader(env, deps, stateDir, options = {}) {
|
|
18330
|
+
const verify = deps.verifyRelease;
|
|
18331
|
+
if (deps.latestVersion !== void 0 || verify === void 0) return null;
|
|
18332
|
+
const sources = {
|
|
18333
|
+
registry: deps.registry ?? npmRegistry(env),
|
|
18334
|
+
verify,
|
|
18335
|
+
stateDir,
|
|
18336
|
+
tarballs: deps.tarballs
|
|
18337
|
+
};
|
|
18338
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
18339
|
+
const declarationOf = (packageName, version) => {
|
|
18340
|
+
const key = `${packageName}@${version}`;
|
|
18341
|
+
let read = declarations.get(key);
|
|
18342
|
+
if (read === void 0) {
|
|
18343
|
+
const left = Math.min(options.deadlineMs ?? 3e4, options.deadline === void 0 ? Number.POSITIVE_INFINITY : options.deadline - Date.now());
|
|
18344
|
+
read = left > 0 ? withinDeadline((signal) => targetDeclaration(sources, packageName, version, { signal }), version, left) : Promise.resolve({
|
|
18345
|
+
known: false,
|
|
18346
|
+
version
|
|
18347
|
+
});
|
|
18348
|
+
declarations.set(key, read);
|
|
18349
|
+
}
|
|
18350
|
+
return read;
|
|
17714
18351
|
};
|
|
17715
18352
|
return async (packageName, version, from = null) => {
|
|
17716
|
-
const
|
|
17717
|
-
return {
|
|
17718
|
-
security,
|
|
17719
|
-
needsDecision
|
|
18353
|
+
const facts = factsOf(version, declaredFor(await declarationOf(packageName, version), from));
|
|
18354
|
+
return facts === null ? null : {
|
|
18355
|
+
security: facts.security,
|
|
18356
|
+
needsDecision: facts.needsDecision
|
|
17720
18357
|
};
|
|
17721
18358
|
};
|
|
17722
18359
|
}
|
|
@@ -17752,25 +18389,34 @@ const registrationKey = (entry) => [
|
|
|
17752
18389
|
* when this is that background check's own finding, is the claim it was made under: the finding is dropped when the
|
|
17753
18390
|
* file no longer holds it, because something newer — a check a person asked for, an update — was written meanwhile.
|
|
17754
18391
|
* The machine it describes was read before that, and put back it would say "update" where "restart" is right.
|
|
18392
|
+
*
|
|
18393
|
+
* Says how it went: `written`; `busy`, when the file's lock stayed held past its bound, so an update can try once
|
|
18394
|
+
* more (design 2026-10-22, R2); and `unchanged` for everything else — switched off, a claim given up, or a write that
|
|
18395
|
+
* failed for any other reason, each left as it always was.
|
|
17755
18396
|
*/
|
|
17756
18397
|
async function recordFound(core, env, report, deps, after = null, claim, release) {
|
|
17757
18398
|
const latest = report.latest[CORE_PACKAGE$1];
|
|
17758
|
-
if (updateCheckSwitchedOff(env) !== null || latest === void 0 || !isVersion(latest)) return;
|
|
18399
|
+
if (updateCheckSwitchedOff(env) !== null || latest === void 0 || !isVersion(latest)) return "unchanged";
|
|
17759
18400
|
const at = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
17760
18401
|
const { behind, current } = updateCheckFindings(report, latest, after);
|
|
17761
18402
|
const moves = movesLeft(report, after);
|
|
17762
|
-
const facts = (record) => release
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
18403
|
+
const facts = (record) => release ?? (record.release?.version === latest ? record.release : null);
|
|
18404
|
+
try {
|
|
18405
|
+
const { written } = await changeUpdateCheck(core.paths.stateDir, (record) => claim !== void 0 && record.checking !== claim ? null : { record: {
|
|
18406
|
+
...record,
|
|
18407
|
+
lastChecked: at,
|
|
18408
|
+
latest,
|
|
18409
|
+
behind,
|
|
18410
|
+
current,
|
|
18411
|
+
lastError: null,
|
|
18412
|
+
checking: null,
|
|
18413
|
+
release: facts(record),
|
|
18414
|
+
moves
|
|
18415
|
+
} });
|
|
18416
|
+
return written ? "written" : "unchanged";
|
|
18417
|
+
} catch (error) {
|
|
18418
|
+
return toCommsError(error).code === "LOCK_TIMEOUT" ? "busy" : "unchanged";
|
|
18419
|
+
}
|
|
17774
18420
|
}
|
|
17775
18421
|
/**
|
|
17776
18422
|
* How many managed registrations still name a versioned runtime once `after` is done, for the notice (design
|
|
@@ -17820,7 +18466,7 @@ function updateCheckFindings(report, latest, after = null) {
|
|
|
17820
18466
|
const globals = items.filter((item) => item.kind === "global");
|
|
17821
18467
|
const global = CHANNELS.filter((channel) => {
|
|
17822
18468
|
const name = serverFacts(channel).packageName;
|
|
17823
|
-
return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.version));
|
|
18469
|
+
return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.reinstall ? null : item.version));
|
|
17824
18470
|
});
|
|
17825
18471
|
const stillBehind = after === null ? report.behind.length > 0 : !(after.ok && after.manual.length === 0 && after.status !== "manual");
|
|
17826
18472
|
const unknown = report.unreadable.length > 0 || report.unpinned.length > 0;
|
|
@@ -17931,7 +18577,7 @@ async function decideNotice(options, where) {
|
|
|
17931
18577
|
};
|
|
17932
18578
|
};
|
|
17933
18579
|
if (due(await readUpdateCheck(options.core.paths.stateDir), now()) === null) return null;
|
|
17934
|
-
|
|
18580
|
+
const decide = (record) => {
|
|
17935
18581
|
const at = now();
|
|
17936
18582
|
const decision = due(record, at);
|
|
17937
18583
|
if (decision === null) return null;
|
|
@@ -17955,7 +18601,8 @@ async function decideNotice(options, where) {
|
|
|
17955
18601
|
}
|
|
17956
18602
|
}
|
|
17957
18603
|
};
|
|
17958
|
-
}
|
|
18604
|
+
};
|
|
18605
|
+
return (await changeUpdateCheck(options.core.paths.stateDir, decide, UPDATE_NOTICE_LOCK)).result ?? null;
|
|
17959
18606
|
}
|
|
17960
18607
|
/**
|
|
17961
18608
|
* What a background run left, when it is this process's to say: an install only to a process running a release before
|
|
@@ -18123,7 +18770,7 @@ async function securityNotice(options) {
|
|
|
18123
18770
|
record: withSecurityShown(record, now, approvalId),
|
|
18124
18771
|
result: now
|
|
18125
18772
|
};
|
|
18126
|
-
});
|
|
18773
|
+
}, UPDATE_NOTICE_LOCK);
|
|
18127
18774
|
if (decided.result === void 0) return null;
|
|
18128
18775
|
shown = decided.result;
|
|
18129
18776
|
}
|
|
@@ -18279,7 +18926,7 @@ async function checkForUpdates(core, env, options = {}) {
|
|
|
18279
18926
|
asked: false,
|
|
18280
18927
|
record: await readUpdateCheck(stateDir)
|
|
18281
18928
|
};
|
|
18282
|
-
await askUnderClaim(core, env, claimedAt, options);
|
|
18929
|
+
if (!(options.handOff !== void 0 && options.deps === void 0 && await handToCore(core, env, claimedAt, options.handOff, Date.now() + 12e4))) await askUnderClaim(core, env, claimedAt, options);
|
|
18283
18930
|
return {
|
|
18284
18931
|
asked: true,
|
|
18285
18932
|
record: await readUpdateCheck(stateDir)
|
|
@@ -18300,6 +18947,11 @@ async function checkForUpdates(core, env, options = {}) {
|
|
|
18300
18947
|
* claim written before anyone asks, so of two processes that both found it a day old, one asks. The time is read under
|
|
18301
18948
|
* the lock too: read before it, the second process's time could be earlier than the first's claim, which reads as a
|
|
18302
18949
|
* clock gone back.
|
|
18950
|
+
*
|
|
18951
|
+
* A claim is best effort, as a notice is (`UPDATE_NOTICE_LOCK`): 2 s at most for the lock, however often it changes
|
|
18952
|
+
* hands meanwhile, then null, and a later process claims. Every command's and every tool call's check claims here,
|
|
18953
|
+
* before the person's output is done; waited out holder by holder, as a record that matters waits, a busy machine's
|
|
18954
|
+
* refreshes kept a finished command's process and its stdout open for up to 15 s (0.26.0 review B).
|
|
18303
18955
|
*/
|
|
18304
18956
|
async function claimUpdateCheck(core, options = {}) {
|
|
18305
18957
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -18316,7 +18968,7 @@ async function claimUpdateCheck(core, options = {}) {
|
|
|
18316
18968
|
},
|
|
18317
18969
|
result: checking
|
|
18318
18970
|
};
|
|
18319
|
-
}).catch(() => null))?.result ?? null;
|
|
18971
|
+
}, UPDATE_NOTICE_LOCK).catch(() => null))?.result ?? null;
|
|
18320
18972
|
}
|
|
18321
18973
|
/**
|
|
18322
18974
|
* The ask, under a claim already taken (`claimUpdateCheck`): the registry, then `comms_update`'s own check. What it
|
|
@@ -18341,7 +18993,7 @@ async function ask(core, env, options) {
|
|
|
18341
18993
|
*/
|
|
18342
18994
|
const settle = (found) => changeUpdateCheck(core.paths.stateDir, (record) => record.checking === options.claimedAt ? { record: {
|
|
18343
18995
|
...record,
|
|
18344
|
-
...found,
|
|
18996
|
+
...typeof found === "function" ? found(record) : found,
|
|
18345
18997
|
lastChecked: options.claimedAt,
|
|
18346
18998
|
checking: null
|
|
18347
18999
|
} } : null).catch(() => void 0);
|
|
@@ -18363,12 +19015,12 @@ async function ask(core, env, options) {
|
|
|
18363
19015
|
});
|
|
18364
19016
|
return;
|
|
18365
19017
|
}
|
|
18366
|
-
const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps);
|
|
18367
|
-
const release = read === null ?
|
|
19018
|
+
const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps, core.paths.stateDir, { deadline: Date.parse(options.claimedAt) + DECLARATION_DEADLINE_MS });
|
|
19019
|
+
const release = read === null ? void 0 : await read(CORE_PACKAGE, latest, VERSION).then((facts) => facts === null ? null : {
|
|
18368
19020
|
version: latest,
|
|
18369
19021
|
security: facts.security,
|
|
18370
19022
|
needsDecision: facts.needsDecision
|
|
18371
|
-
}
|
|
19023
|
+
}, () => null);
|
|
18372
19024
|
try {
|
|
18373
19025
|
await updateCheck(core, env, {
|
|
18374
19026
|
...deps,
|
|
@@ -18377,13 +19029,13 @@ async function ask(core, env, options) {
|
|
|
18377
19029
|
now: options.now
|
|
18378
19030
|
}, options.claimedAt);
|
|
18379
19031
|
} catch (error) {
|
|
18380
|
-
await settle({
|
|
19032
|
+
await settle((record) => ({
|
|
18381
19033
|
latest,
|
|
18382
19034
|
behind: null,
|
|
18383
19035
|
current: null,
|
|
18384
19036
|
lastError: reason(error),
|
|
18385
|
-
release
|
|
18386
|
-
});
|
|
19037
|
+
release: release ?? (record.release?.version === latest ? record.release : null)
|
|
19038
|
+
}));
|
|
18387
19039
|
}
|
|
18388
19040
|
}
|
|
18389
19041
|
/**
|
|
@@ -18502,8 +19154,13 @@ function updateCheckChildEnvironment(env, platform = process.platform) {
|
|
|
18502
19154
|
* message — or whatever reads that output would wait for the child as it waited for the check. Detached, so a
|
|
18503
19155
|
* Ctrl-C at the terminal does not cut the ask short, and hidden, so Windows opens no console for it.
|
|
18504
19156
|
*
|
|
18505
|
-
*
|
|
18506
|
-
*
|
|
19157
|
+
* A channel's command hands the ask to core's CLI first (design 2026-10-22, P2): core's command, located at the
|
|
19158
|
+
* channel's own version, with the same hidden command, the same claim and the same environment. Core's child verifies
|
|
19159
|
+
* what a release declares, which the channel's own could not.
|
|
19160
|
+
*
|
|
19161
|
+
* When the child cannot be started — no core, no entry to start, a program not there or not allowed, a `.cmd` that
|
|
19162
|
+
* would need a shell, a launcher whose Node was removed, which starts and ends at once — the next is tried: the
|
|
19163
|
+
* channel's own child, then the check in this process, as it was before. It never throws.
|
|
18507
19164
|
*/
|
|
18508
19165
|
async function handUpdateCheckToChild(core, env, options) {
|
|
18509
19166
|
const deadline = Date.now() + TERMINAL_CHECK_WAIT_MS;
|
|
@@ -18515,59 +19172,127 @@ async function handUpdateCheckToChild(core, env, options) {
|
|
|
18515
19172
|
intervalMs: enabled.intervalMs
|
|
18516
19173
|
});
|
|
18517
19174
|
if (claimedAt === null) return;
|
|
19175
|
+
if (options.handOff !== void 0 && await handToCore(core, env, claimedAt, options.handOff, deadline)) return;
|
|
18518
19176
|
const entry = options.entry === void 0 ? updateCheckChildEntry() : options.entry;
|
|
18519
|
-
if (!(entry !== null && await
|
|
19177
|
+
if (!(entry !== null && await startedSafely(options.handOff?.start ?? startCheckChild, ownChildWords(entry, claimedAt, core.paths), updateCheckChildEnvironment(env), deadline))) await askUnderClaim(core, env, claimedAt, {
|
|
18520
19178
|
now: options.now,
|
|
18521
19179
|
...options.verifyRelease ? { deps: { verifyRelease: options.verifyRelease } } : {}
|
|
18522
19180
|
});
|
|
18523
19181
|
} catch {}
|
|
18524
19182
|
}
|
|
19183
|
+
/** This CLI again, as its child: the entry, every folder pinned, the hidden command and the claim. */
|
|
19184
|
+
function ownChildWords(entry, claimedAt, paths) {
|
|
19185
|
+
const pathArgs = PATH_OPTIONS.flatMap(({ key, flag }) => [flag, paths[key]]);
|
|
19186
|
+
return [
|
|
19187
|
+
entry.command,
|
|
19188
|
+
...entry.args,
|
|
19189
|
+
...pathArgs,
|
|
19190
|
+
UPDATE_CHECK_CHILD_COMMAND,
|
|
19191
|
+
claimedAt
|
|
19192
|
+
];
|
|
19193
|
+
}
|
|
18525
19194
|
/**
|
|
18526
|
-
*
|
|
18527
|
-
*
|
|
19195
|
+
* Core's own command for `update-check-child <claimedAt>`, as the words to start: `core.handoffs.core(…)` with every
|
|
19196
|
+
* folder pinned, located as the channel finds core — its installed dependency, at exactly the channel's own version,
|
|
19197
|
+
* through the stable way in a managed runtime keeps (`locateCliCommand`). Locating checks files and runs nothing.
|
|
19198
|
+
*
|
|
19199
|
+
* Located for this process's own system, whatever shell its printed commands are quoted for: these words are started
|
|
19200
|
+
* here, and a managed runtime's stable launcher is started one way on Windows and another elsewhere.
|
|
19201
|
+
*
|
|
19202
|
+
* Null for core's own process, which asks itself and is never handed its own check; for a core opened without its
|
|
19203
|
+
* caller, which can locate nothing; and where no such core is found.
|
|
18528
19204
|
*/
|
|
18529
|
-
|
|
19205
|
+
function coreUpdateCheckWords(core, claimedAt) {
|
|
19206
|
+
if (core.handoffs === void 0 || core.handoffs.caller.packageName === CORE_PACKAGE) return null;
|
|
19207
|
+
try {
|
|
19208
|
+
const handoff = requireHandoffs(core, process.platform).core([UPDATE_CHECK_CHILD_COMMAND, claimedAt], { downloads: true });
|
|
19209
|
+
return isCommand(handoff) ? handoff.words : null;
|
|
19210
|
+
} catch {
|
|
19211
|
+
return null;
|
|
19212
|
+
}
|
|
19213
|
+
}
|
|
19214
|
+
/**
|
|
19215
|
+
* The ask handed to core's CLI (design 2026-10-22, P2): its command located (`handOff.locate`), started with the
|
|
19216
|
+
* claim and this command's child environment (`handOff.start`), and waited for until `deadline`. False when there is
|
|
19217
|
+
* no such command, or it did not start: then nothing was asked, and the claim is still this process's to ask under. It
|
|
19218
|
+
* never throws.
|
|
19219
|
+
*/
|
|
19220
|
+
async function handToCore(core, env, claimedAt, handOff, deadline) {
|
|
19221
|
+
let words;
|
|
19222
|
+
try {
|
|
19223
|
+
words = (handOff.locate ?? coreUpdateCheckWords)(core, claimedAt);
|
|
19224
|
+
} catch {
|
|
19225
|
+
return false;
|
|
19226
|
+
}
|
|
19227
|
+
if (words === null || words.length === 0) return false;
|
|
19228
|
+
return startedSafely(handOff.start ?? startCheckChild, words, updateCheckChildEnvironment(env), deadline);
|
|
19229
|
+
}
|
|
19230
|
+
/** A start's answer, with one that throws counted as not started: the fallback after it is all that is left. */
|
|
19231
|
+
async function startedSafely(start, words, env, deadline) {
|
|
19232
|
+
try {
|
|
19233
|
+
return await start(words, env, deadline) === true;
|
|
19234
|
+
} catch {
|
|
19235
|
+
return false;
|
|
19236
|
+
}
|
|
19237
|
+
}
|
|
19238
|
+
/**
|
|
19239
|
+
* Starts `words` — a program, then its arguments — detached and hidden, with no shell and `env` as given, and waits,
|
|
19240
|
+
* until `deadline` at most, for it to say the ask is over, or to end. False when it could not be started at all, or
|
|
19241
|
+
* when it ended with a code other than 0, or by a signal, before it said the ask is over — a launcher whose Node was
|
|
19242
|
+
* removed starts, and ends at once with 127 (0.26.0 review B). Either way nothing was asked, and the claim is still
|
|
19243
|
+
* this process's to ask under.
|
|
19244
|
+
*
|
|
19245
|
+
* Never through a shell: each word is one argument, as located, and nothing in them is read as a shell would read it.
|
|
19246
|
+
* A program only a shell could start — a Windows `.cmd` or `.bat`, which Node refuses without one (EINVAL) — is not
|
|
19247
|
+
* started, and the caller falls back.
|
|
19248
|
+
*/
|
|
19249
|
+
async function startCheckChild(words, env, deadline) {
|
|
19250
|
+
const [command, ...args] = words;
|
|
19251
|
+
if (command === void 0) return false;
|
|
18530
19252
|
let child;
|
|
18531
19253
|
try {
|
|
18532
|
-
|
|
18533
|
-
child = spawn(entry.command, [
|
|
18534
|
-
...entry.args,
|
|
18535
|
-
...pathArgs,
|
|
18536
|
-
UPDATE_CHECK_CHILD_COMMAND,
|
|
18537
|
-
claimedAt
|
|
18538
|
-
], {
|
|
19254
|
+
child = spawn(command, args, {
|
|
18539
19255
|
detached: true,
|
|
18540
19256
|
windowsHide: true,
|
|
19257
|
+
shell: false,
|
|
18541
19258
|
stdio: [
|
|
18542
19259
|
"ignore",
|
|
18543
19260
|
"ignore",
|
|
18544
19261
|
"ignore",
|
|
18545
19262
|
"ipc"
|
|
18546
19263
|
],
|
|
18547
|
-
env
|
|
19264
|
+
env
|
|
18548
19265
|
});
|
|
18549
19266
|
} catch {
|
|
18550
19267
|
return false;
|
|
18551
19268
|
}
|
|
18552
|
-
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
await new Promise((resolve) => {
|
|
18557
|
-
const timer = setTimeout(resolve, Math.max(0, deadline - Date.now()));
|
|
18558
|
-
const done = () => {
|
|
19269
|
+
child.on("error", () => void 0);
|
|
19270
|
+
const started = await new Promise((resolve) => {
|
|
19271
|
+
const timer = setTimeout(() => resolve(true), Math.max(0, deadline - Date.now()));
|
|
19272
|
+
const done = (started) => {
|
|
18559
19273
|
clearTimeout(timer);
|
|
18560
|
-
resolve();
|
|
19274
|
+
resolve(started);
|
|
18561
19275
|
};
|
|
18562
|
-
child.once("error", done);
|
|
18563
|
-
child.once("exit", done);
|
|
19276
|
+
child.once("error", () => done(child.pid !== void 0));
|
|
18564
19277
|
child.on("message", (message) => {
|
|
18565
|
-
if (message?.type === "settled") done();
|
|
19278
|
+
if (message?.type === "settled") done(true);
|
|
19279
|
+
});
|
|
19280
|
+
let ended = null;
|
|
19281
|
+
const over = () => {
|
|
19282
|
+
if (ended !== null && !child.connected) done(ended.code === 0 && ended.signal === null);
|
|
19283
|
+
};
|
|
19284
|
+
child.once("exit", (code, signal) => {
|
|
19285
|
+
ended = {
|
|
19286
|
+
code,
|
|
19287
|
+
signal
|
|
19288
|
+
};
|
|
19289
|
+
over();
|
|
18566
19290
|
});
|
|
19291
|
+
child.once("disconnect", over);
|
|
18567
19292
|
});
|
|
18568
19293
|
if (child.connected) child.disconnect();
|
|
18569
19294
|
child.unref();
|
|
18570
|
-
return
|
|
19295
|
+
return started;
|
|
18571
19296
|
}
|
|
18572
19297
|
/**
|
|
18573
19298
|
* The hidden command's work, in the child a command started (`UPDATE_CHECK_CHILD_COMMAND`): the ask, under the claim
|
|
@@ -18579,7 +19304,7 @@ async function startUpdateCheckChild(entry, claimedAt, paths, env, deadline) {
|
|
|
18579
19304
|
async function runUpdateCheckChild(core, env, claimedAt, options = {}) {
|
|
18580
19305
|
if (claimedAt === void 0 || !Number.isFinite(Date.parse(claimedAt))) throw new CommsError("USAGE", `${UPDATE_CHECK_CHILD_COMMAND} takes the time of the claim it asks under`, { hint: handoffSentence(requireHandoffs(core).core(["update", "--check"]), (command) => `Nothing runs this but a command finishing the day’s update check; ${command} asks now.`) });
|
|
18581
19306
|
try {
|
|
18582
|
-
if ((await readUpdateCheck(core.paths.stateDir)).checking === claimedAt) await askUnderClaim(core, env, claimedAt, options.
|
|
19307
|
+
if ((await readUpdateCheck(core.paths.stateDir)).checking === claimedAt) await askUnderClaim(core, env, claimedAt, options.deps !== void 0 ? { deps: options.deps } : {});
|
|
18583
19308
|
} finally {
|
|
18584
19309
|
settled();
|
|
18585
19310
|
}
|
|
@@ -18608,12 +19333,13 @@ function terminalUpdateHooks(core, env, options = {}) {
|
|
|
18608
19333
|
await handUpdateCheckToChild(core, env, {
|
|
18609
19334
|
now: options.now,
|
|
18610
19335
|
entry: options.childEntry,
|
|
18611
|
-
verifyRelease: options.verifyRelease
|
|
19336
|
+
verifyRelease: options.verifyRelease,
|
|
19337
|
+
handOff: options.handOff
|
|
18612
19338
|
});
|
|
18613
19339
|
} };
|
|
18614
19340
|
}
|
|
18615
19341
|
//#endregion
|
|
18616
|
-
//#region ../core/dist/tool-arguments-
|
|
19342
|
+
//#region ../core/dist/tool-arguments-CJ4RQpx4.mjs
|
|
18617
19343
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
18618
19344
|
const TARGET = "draft-2020-12";
|
|
18619
19345
|
/**
|
|
@@ -19002,7 +19728,7 @@ function spoken(words, joiner) {
|
|
|
19002
19728
|
return `${words.slice(0, -1).join(", ")} ${joiner} ${words.at(-1)}`;
|
|
19003
19729
|
}
|
|
19004
19730
|
//#endregion
|
|
19005
|
-
//#region ../../node_modules/.pnpm/postal-mime@4.
|
|
19731
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/decode-strings.js
|
|
19006
19732
|
const textEncoder = new TextEncoder();
|
|
19007
19733
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
19008
19734
|
const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
|
|
@@ -19101,20 +19827,51 @@ const ENCODING_LABELS = new Set([
|
|
|
19101
19827
|
"cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
|
|
19102
19828
|
"windows-949",
|
|
19103
19829
|
"csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
|
|
19104
|
-
"unicodefffe utf-16be",
|
|
19105
|
-
"csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
|
|
19106
19830
|
"x-user-defined"
|
|
19107
19831
|
].join(" ").split(" "));
|
|
19832
|
+
const REPLACEMENT_LABELS = /* @__PURE__ */ new Set([
|
|
19833
|
+
"csiso2022kr",
|
|
19834
|
+
"hz-gb-2312",
|
|
19835
|
+
"iso-2022-cn",
|
|
19836
|
+
"iso-2022-cn-ext",
|
|
19837
|
+
"iso-2022-kr",
|
|
19838
|
+
"replacement"
|
|
19839
|
+
]);
|
|
19840
|
+
const replacementDecoder = {
|
|
19841
|
+
encoding: "replacement",
|
|
19842
|
+
decode: (input) => input && input.byteLength ? "�" : ""
|
|
19843
|
+
};
|
|
19844
|
+
const UTF16_LABELS = /* @__PURE__ */ new Map([
|
|
19845
|
+
["utf-16", "utf-16"],
|
|
19846
|
+
["utf16", "utf-16"],
|
|
19847
|
+
["utf_16", "utf-16"],
|
|
19848
|
+
["utf-16le", "utf-16le"],
|
|
19849
|
+
["utf-16be", "utf-16be"]
|
|
19850
|
+
]);
|
|
19851
|
+
/**
|
|
19852
|
+
* The label of the Encoding Standard a charset resolves to, as written or through the
|
|
19853
|
+
* alias tables, or null when it names no encoding the standard lists
|
|
19854
|
+
*/
|
|
19855
|
+
function resolveLabel(charset) {
|
|
19856
|
+
charset = (charset || "utf8").trim().toLowerCase();
|
|
19857
|
+
if (ENCODING_LABELS.has(charset)) return charset;
|
|
19858
|
+
const utf16 = UTF16_LABELS.get(charset);
|
|
19859
|
+
if (utf16) return utf16;
|
|
19860
|
+
const normalized = normalizeCharset(charset);
|
|
19861
|
+
const codePage = normalized.match(codePagePattern);
|
|
19862
|
+
const alias = codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized;
|
|
19863
|
+
return ENCODING_LABELS.has(alias) ? alias : null;
|
|
19864
|
+
}
|
|
19108
19865
|
const decoders = /* @__PURE__ */ new Map();
|
|
19109
|
-
function tryDecoder(
|
|
19110
|
-
if (
|
|
19111
|
-
let decoder = decoders.get(
|
|
19866
|
+
function tryDecoder(label) {
|
|
19867
|
+
if (REPLACEMENT_LABELS.has(label)) return replacementDecoder;
|
|
19868
|
+
let decoder = decoders.get(label);
|
|
19112
19869
|
if (decoder === void 0) {
|
|
19113
19870
|
decoder = null;
|
|
19114
19871
|
try {
|
|
19115
|
-
decoder = new TextDecoder(
|
|
19872
|
+
decoder = new TextDecoder(label);
|
|
19116
19873
|
} catch {}
|
|
19117
|
-
decoders.set(
|
|
19874
|
+
decoders.set(label, decoder);
|
|
19118
19875
|
}
|
|
19119
19876
|
return decoder;
|
|
19120
19877
|
}
|
|
@@ -19123,16 +19880,21 @@ const utf8Decoder = new TextDecoder();
|
|
|
19123
19880
|
* Finds the decoder for a charset label, or null when the runtime has no encoding for it
|
|
19124
19881
|
*/
|
|
19125
19882
|
function findDecoder(charset) {
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
if (decoder) return decoder;
|
|
19129
|
-
const normalized = normalizeCharset(charset);
|
|
19130
|
-
const codePage = normalized.match(codePagePattern);
|
|
19131
|
-
return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized);
|
|
19883
|
+
const label = resolveLabel(charset);
|
|
19884
|
+
return label ? tryDecoder(label) : null;
|
|
19132
19885
|
}
|
|
19133
19886
|
function getDecoder(charset) {
|
|
19134
19887
|
return findDecoder(charset) || tryDecoder("windows-1252") || utf8Decoder;
|
|
19135
19888
|
}
|
|
19889
|
+
const UTF16_UNORDERED_LABELS = /* @__PURE__ */ new Set(["utf-16"]);
|
|
19890
|
+
/**
|
|
19891
|
+
* The byte order a UTF-16 byte order mark at the start of the bytes names, or null without one
|
|
19892
|
+
*/
|
|
19893
|
+
function bomByteOrder(bytes) {
|
|
19894
|
+
if (bytes[0] === 254 && bytes[1] === 255) return "utf-16be";
|
|
19895
|
+
if (bytes[0] === 255 && bytes[1] === 254) return "utf-16le";
|
|
19896
|
+
return null;
|
|
19897
|
+
}
|
|
19136
19898
|
/**
|
|
19137
19899
|
* Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
|
|
19138
19900
|
* the exception one throws costs more than the decode itself and a message decides how
|
|
@@ -19186,8 +19948,14 @@ function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
|
|
|
19186
19948
|
* @return Decoded text
|
|
19187
19949
|
*/
|
|
19188
19950
|
function decodeCharset(bytes, charset) {
|
|
19189
|
-
const
|
|
19190
|
-
|
|
19951
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
19952
|
+
const label = resolveLabel(charset);
|
|
19953
|
+
let decoder = label ? tryDecoder(label) : null;
|
|
19954
|
+
if (label && UTF16_UNORDERED_LABELS.has(label)) {
|
|
19955
|
+
const order = bomByteOrder(view);
|
|
19956
|
+
decoder = order && tryDecoder(order) || decoder;
|
|
19957
|
+
}
|
|
19958
|
+
return decoder ? decoder.decode(view) : decodeUtf8OrWindows1252(view);
|
|
19191
19959
|
}
|
|
19192
19960
|
/**
|
|
19193
19961
|
* Numeric value of an ASCII hex digit
|
|
@@ -19307,7 +20075,7 @@ function splitEncodedWords(str) {
|
|
|
19307
20075
|
function canJoinWords(left, right) {
|
|
19308
20076
|
const encoding = left.encoding.toUpperCase();
|
|
19309
20077
|
if (left.charset !== right.charset || encoding !== right.encoding.toUpperCase()) return false;
|
|
19310
|
-
if (encoding === "B") return left.encodedText.length % 4 === 0 &&
|
|
20078
|
+
if (encoding === "B") return left.encodedText.length % 4 === 0 && !left.padded;
|
|
19311
20079
|
return true;
|
|
19312
20080
|
}
|
|
19313
20081
|
/**
|
|
@@ -19334,13 +20102,15 @@ function renderTokens(tokens, joinWords) {
|
|
|
19334
20102
|
}
|
|
19335
20103
|
if (pending && joinWords && canJoinWords(pending, token)) {
|
|
19336
20104
|
pending.encodedText += token.encodedText;
|
|
20105
|
+
pending.padded = token.encodedText.endsWith("=");
|
|
19337
20106
|
continue;
|
|
19338
20107
|
}
|
|
19339
20108
|
if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
19340
20109
|
pending = {
|
|
19341
20110
|
charset: token.charset,
|
|
19342
20111
|
encoding: token.encoding,
|
|
19343
|
-
encodedText: token.encodedText
|
|
20112
|
+
encodedText: token.encodedText,
|
|
20113
|
+
padded: token.encodedText.endsWith("=")
|
|
19344
20114
|
};
|
|
19345
20115
|
}
|
|
19346
20116
|
if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
@@ -19359,7 +20129,86 @@ function decodeWords(str) {
|
|
|
19359
20129
|
}
|
|
19360
20130
|
new TextDecoder("utf-8", { ignoreBOM: true });
|
|
19361
20131
|
//#endregion
|
|
19362
|
-
//#region ../../node_modules/.pnpm/postal-mime@4.
|
|
20132
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/text-format.js
|
|
20133
|
+
const hexByte = (value) => value.toString(16).toUpperCase().padStart(2, "0");
|
|
20134
|
+
Array.from({ length: 256 }, (_, byte) => "%" + hexByte(byte));
|
|
20135
|
+
//#endregion
|
|
20136
|
+
//#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/address-parser.js
|
|
20137
|
+
/** The token is an operator, a text run otherwise */
|
|
20138
|
+
const TOKEN_OPERATOR = 1;
|
|
20139
|
+
/** The next character continues the same run without whitespace in between */
|
|
20140
|
+
const TOKEN_NO_BREAK = 2;
|
|
20141
|
+
/** The text run holds characters its value leaves out or changes, so it is no plain slice */
|
|
20142
|
+
const TOKEN_DIRTY = 4;
|
|
20143
|
+
/** A backslash escapes the next character in the text run, inside a quoted string or a comment */
|
|
20144
|
+
const TOKEN_ESCAPES = 8;
|
|
20145
|
+
/** The text run opens on a character an escaping backslash in front of it has escaped */
|
|
20146
|
+
const TOKEN_STARTS_ESCAPED = 16;
|
|
20147
|
+
/**
|
|
20148
|
+
* The tokens of an address field, kept as offsets into it.
|
|
20149
|
+
*
|
|
20150
|
+
* Every token used to be an object of its own, an operator for every ',' and a value string
|
|
20151
|
+
* for every run of text, and a list of one letter names, 'a,a,a,...', took some 65 times its
|
|
20152
|
+
* size in tokens before any address was read. A token is now three numbers in one typed
|
|
20153
|
+
* array, its kind and the range of the field it covers, and its value is sliced off the field
|
|
20154
|
+
* when it is read. The few text runs whose value differs from their slice, as they hold an
|
|
20155
|
+
* escaping backslash, a line break or a control character, are rebuilt once and kept aside.
|
|
20156
|
+
*/
|
|
20157
|
+
var TokenList = class {
|
|
20158
|
+
constructor(str) {
|
|
20159
|
+
this.str = str;
|
|
20160
|
+
this.slots = /* @__PURE__ */ new Int32Array(48);
|
|
20161
|
+
this.length = 0;
|
|
20162
|
+
this.cleaned = /* @__PURE__ */ new Map();
|
|
20163
|
+
}
|
|
20164
|
+
/**
|
|
20165
|
+
* Appends a token
|
|
20166
|
+
*
|
|
20167
|
+
* @param kind Kind and flags of the token
|
|
20168
|
+
* @param start Offset of its first character
|
|
20169
|
+
* @param end Offset just past its last character
|
|
20170
|
+
* @return Index of the token
|
|
20171
|
+
*/
|
|
20172
|
+
push(kind, start, end) {
|
|
20173
|
+
if ((this.length + 1) * 3 > this.slots.length) {
|
|
20174
|
+
const grown = new Int32Array(this.slots.length * 2);
|
|
20175
|
+
grown.set(this.slots);
|
|
20176
|
+
this.slots = grown;
|
|
20177
|
+
}
|
|
20178
|
+
const at = this.length * 3;
|
|
20179
|
+
this.slots[at] = kind;
|
|
20180
|
+
this.slots[at + 1] = start;
|
|
20181
|
+
this.slots[at + 2] = end;
|
|
20182
|
+
return this.length++;
|
|
20183
|
+
}
|
|
20184
|
+
/**
|
|
20185
|
+
* The character of an operator token, an empty string for a text run
|
|
20186
|
+
*/
|
|
20187
|
+
operatorAt(index) {
|
|
20188
|
+
const at = index * 3;
|
|
20189
|
+
return this.slots[at] & TOKEN_OPERATOR ? this.str.charAt(this.slots[at + 1]) : "";
|
|
20190
|
+
}
|
|
20191
|
+
/**
|
|
20192
|
+
* Whether a token is an operator
|
|
20193
|
+
*/
|
|
20194
|
+
isOperator(index) {
|
|
20195
|
+
return (this.slots[index * 3] & TOKEN_OPERATOR) !== 0;
|
|
20196
|
+
}
|
|
20197
|
+
/**
|
|
20198
|
+
* Whether the next character continues the run of a token without whitespace in between
|
|
20199
|
+
*/
|
|
20200
|
+
noBreak(index) {
|
|
20201
|
+
return (this.slots[index * 3] & TOKEN_NO_BREAK) !== 0;
|
|
20202
|
+
}
|
|
20203
|
+
/**
|
|
20204
|
+
* Value of a token
|
|
20205
|
+
*/
|
|
20206
|
+
value(index) {
|
|
20207
|
+
const cleaned = this.cleaned.get(index);
|
|
20208
|
+
if (cleaned !== void 0) return cleaned;
|
|
20209
|
+
return this.str.slice(this.slots[index * 3 + 1], this.slots[index * 3 + 2]);
|
|
20210
|
+
}
|
|
20211
|
+
};
|
|
19363
20212
|
/**
|
|
19364
20213
|
* Any whitespace, the test for an angle-addr that needs recovering
|
|
19365
20214
|
*/
|
|
@@ -19374,6 +20223,23 @@ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
|
19374
20223
|
*/
|
|
19375
20224
|
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
19376
20225
|
/**
|
|
20226
|
+
* The same with a domain literal for the domain, which may hold an '@' of its own
|
|
20227
|
+
*/
|
|
20228
|
+
const LITERAL_ADDR_SPEC = /^[^@\s]+@\[[^[\]\\\s]*\]$/;
|
|
20229
|
+
/**
|
|
20230
|
+
* The '@' and domain that follow a quoted local part in the same run
|
|
20231
|
+
*/
|
|
20232
|
+
const QUOTED_LOCAL_DOMAIN = /^@(?:[^@\s"]+|\[[^[\]\\\s]*\])$/;
|
|
20233
|
+
/**
|
|
20234
|
+
* Whether a whitespace free run reads as an addr-spec under the strict grammar
|
|
20235
|
+
*
|
|
20236
|
+
* @param value Run to check
|
|
20237
|
+
* @return true when it does
|
|
20238
|
+
*/
|
|
20239
|
+
function isAddrSpec(value) {
|
|
20240
|
+
return ADDR_SPEC.test(value) || LITERAL_ADDR_SPEC.test(value);
|
|
20241
|
+
}
|
|
20242
|
+
/**
|
|
19377
20243
|
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
19378
20244
|
* further '@' that a domain should not have but malformed headers carry anyway
|
|
19379
20245
|
*/
|
|
@@ -19384,6 +20250,22 @@ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
|
19384
20250
|
*/
|
|
19385
20251
|
const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
19386
20252
|
/**
|
|
20253
|
+
* A domain literal that ends an address. dtext includes '@' (RFC 5322 3.4.1), so the '@'
|
|
20254
|
+
* in front of the literal is the one that splits the domain off, not the last one: reading
|
|
20255
|
+
* 'a@[x@evil.com]' at its last '@' made it an address at evil.com.
|
|
20256
|
+
*/
|
|
20257
|
+
const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
|
|
20258
|
+
/**
|
|
20259
|
+
* Offset of the '@' that splits the domain off an address, or -1 when there is none
|
|
20260
|
+
*
|
|
20261
|
+
* @param address Address to split
|
|
20262
|
+
* @return Offset of the '@'
|
|
20263
|
+
*/
|
|
20264
|
+
function domainAt(address) {
|
|
20265
|
+
const literal = address.charAt(address.length - 1) === "]" ? DOMAIN_LITERAL_TAIL.exec(address) : null;
|
|
20266
|
+
return literal ? address.length - literal[1].length - 1 : address.lastIndexOf("@");
|
|
20267
|
+
}
|
|
20268
|
+
/**
|
|
19387
20269
|
* Restores the quoting of a local part that was read out of a quoted string.
|
|
19388
20270
|
*
|
|
19389
20271
|
* RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
|
|
@@ -19396,7 +20278,7 @@ const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
|
19396
20278
|
* @return Address with the local part as a quoted-string
|
|
19397
20279
|
*/
|
|
19398
20280
|
function quoteLocalPart(address) {
|
|
19399
|
-
const lastAt = address
|
|
20281
|
+
const lastAt = domainAt(address);
|
|
19400
20282
|
if (lastAt < 0) return address;
|
|
19401
20283
|
const user = address.substring(0, lastAt);
|
|
19402
20284
|
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) return address;
|
|
@@ -19433,6 +20315,50 @@ function indexOfAt(text, from, to) {
|
|
|
19433
20315
|
return -1;
|
|
19434
20316
|
}
|
|
19435
20317
|
/**
|
|
20318
|
+
* A quoted-string a value opens with
|
|
20319
|
+
*/
|
|
20320
|
+
const LEADING_QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"/;
|
|
20321
|
+
/**
|
|
20322
|
+
* Drops the whitespace on either side of the '@' at an offset.
|
|
20323
|
+
*
|
|
20324
|
+
* obs-local-part and obs-domain allow CFWS around the '@' (RFC 5322 4.4), so 'user @ good.com'
|
|
20325
|
+
* names user@good.com. Kept, the whitespace became part of the address as '"user "@ good.com'.
|
|
20326
|
+
* The scan is by index, a pattern such as /\s+@/ backtracks over a long whitespace run that
|
|
20327
|
+
* no '@' follows.
|
|
20328
|
+
*
|
|
20329
|
+
* @param value Value holding the '@'
|
|
20330
|
+
* @param at Offset of the '@', or -1
|
|
20331
|
+
* @return The value with the whitespace beside that '@' removed
|
|
20332
|
+
*/
|
|
20333
|
+
function joinAtSpacing(value, at) {
|
|
20334
|
+
if (at < 0) return value;
|
|
20335
|
+
let start = at;
|
|
20336
|
+
while (start > 0 && isSpaceCode(value.charCodeAt(start - 1))) start--;
|
|
20337
|
+
let end = at + 1;
|
|
20338
|
+
while (end < value.length && isSpaceCode(value.charCodeAt(end))) end++;
|
|
20339
|
+
return start === at && end === at + 1 ? value : value.slice(0, start) + "@" + value.slice(end);
|
|
20340
|
+
}
|
|
20341
|
+
/**
|
|
20342
|
+
* Drops the whitespace beside the '@' that splits the domain off a value. That is the '@'
|
|
20343
|
+
* right after a quoted local part the value opens with, or else the only '@' it holds outside
|
|
20344
|
+
* any quoted string. With several it is not clear which one the whitespace belongs to, and
|
|
20345
|
+
* '<x@evil.com @good.com>' is left to the addr-spec recovery to read as it always has.
|
|
20346
|
+
*
|
|
20347
|
+
* @param value Value to clean up
|
|
20348
|
+
* @return The value with the whitespace beside that '@' removed
|
|
20349
|
+
*/
|
|
20350
|
+
function collapseAtSpacing(value) {
|
|
20351
|
+
const quoted = LEADING_QUOTED_STRING.exec(value);
|
|
20352
|
+
if (quoted) {
|
|
20353
|
+
let at = quoted[0].length;
|
|
20354
|
+
while (at < value.length && isSpaceCode(value.charCodeAt(at))) at++;
|
|
20355
|
+
if (value.charAt(at) === "@" && value.indexOf("@", at + 1) < 0) return joinAtSpacing(value, at);
|
|
20356
|
+
}
|
|
20357
|
+
const at = value.indexOf("@");
|
|
20358
|
+
if (at < 0 || value.indexOf("@", at + 1) >= 0 || value.lastIndexOf("\"") > at) return value;
|
|
20359
|
+
return joinAtSpacing(value, at);
|
|
20360
|
+
}
|
|
20361
|
+
/**
|
|
19436
20362
|
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
19437
20363
|
*
|
|
19438
20364
|
* Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
|
|
@@ -19503,6 +20429,7 @@ function looseAddressStart(text) {
|
|
|
19503
20429
|
* @return The address and the display text, with any leftovers moved to the text
|
|
19504
20430
|
*/
|
|
19505
20431
|
function recoverAddrSpec(address, text) {
|
|
20432
|
+
address = collapseAtSpacing(address);
|
|
19506
20433
|
if (!HAS_WHITESPACE.test(address)) return {
|
|
19507
20434
|
address,
|
|
19508
20435
|
text
|
|
@@ -19522,7 +20449,7 @@ function recoverAddrSpec(address, text) {
|
|
|
19522
20449
|
text
|
|
19523
20450
|
};
|
|
19524
20451
|
const parts = address.split(/\s+/);
|
|
19525
|
-
let addrIndex = parts.findIndex(
|
|
20452
|
+
let addrIndex = parts.findIndex(isAddrSpec);
|
|
19526
20453
|
if (addrIndex < 0) addrIndex = parts.findIndex((part) => LOOSE_ADDR_SPEC.test(part));
|
|
19527
20454
|
if (addrIndex < 0) return {
|
|
19528
20455
|
address,
|
|
@@ -19545,7 +20472,16 @@ function recoverAddrSpec(address, text) {
|
|
|
19545
20472
|
* consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
|
|
19546
20473
|
* whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
|
|
19547
20474
|
* the same rule the token walk applies outside the brackets. Quoted strings and domain
|
|
19548
|
-
* literals are copied through untouched, a '(' in there is not a comment.
|
|
20475
|
+
* literals are copied through untouched, a '(' in there is not a comment. An unexpected '<'
|
|
20476
|
+
* outside them drops everything in front of it, comments included.
|
|
20477
|
+
*
|
|
20478
|
+
* RFC 822 also let an angle-addr name the relays to go through in front of the mailbox,
|
|
20479
|
+
* '<@relay1,@relay2:user@example.com>', which RFC 5322 still parses as obs-route. The route
|
|
20480
|
+
* is not part of the mailbox, and folded into the local part it made
|
|
20481
|
+
* '"@relay1,@relay2:user"@example.com' a recipient that does not exist. A local part can not
|
|
20482
|
+
* start with '@', so contents that do, up to the first ':' outside a domain literal and in
|
|
20483
|
+
* front of any quoted string, are a route and are dropped, as long as a mailbox with an '@'
|
|
20484
|
+
* of its own follows.
|
|
19549
20485
|
*
|
|
19550
20486
|
* @param address Contents of the angle brackets
|
|
19551
20487
|
* @return The address with the comments removed, and the text of the comments
|
|
@@ -19557,6 +20493,10 @@ function stripAddressComments(address) {
|
|
|
19557
20493
|
let depth = 0;
|
|
19558
20494
|
let closer = "";
|
|
19559
20495
|
let lastChar = "";
|
|
20496
|
+
let pendingSpace = false;
|
|
20497
|
+
let firstChar = "";
|
|
20498
|
+
let quoteSeen = false;
|
|
20499
|
+
let routeEnd = -1;
|
|
19560
20500
|
for (let i = 0, len = address.length; i < len; i++) {
|
|
19561
20501
|
const chr = address.charAt(i);
|
|
19562
20502
|
if (depth) {
|
|
@@ -19567,13 +20507,17 @@ function stripAddressComments(address) {
|
|
|
19567
20507
|
} else if (chr === ")" && !--depth) {
|
|
19568
20508
|
comments.push(comment.trim());
|
|
19569
20509
|
comment = "";
|
|
19570
|
-
|
|
19571
|
-
result += " ";
|
|
19572
|
-
lastChar = " ";
|
|
19573
|
-
}
|
|
20510
|
+
pendingSpace = lastChar !== "@";
|
|
19574
20511
|
} else comment += chr;
|
|
19575
20512
|
continue;
|
|
19576
20513
|
}
|
|
20514
|
+
if (pendingSpace && chr !== "(") {
|
|
20515
|
+
pendingSpace = false;
|
|
20516
|
+
if (chr !== "@" && chr !== "<" && !isSpaceCode(address.charCodeAt(i))) {
|
|
20517
|
+
result += " ";
|
|
20518
|
+
lastChar = " ";
|
|
20519
|
+
}
|
|
20520
|
+
}
|
|
19577
20521
|
if (closer) {
|
|
19578
20522
|
if (chr === "\\" && closer === "\"" && i < len - 1) {
|
|
19579
20523
|
result += chr + address.charAt(++i);
|
|
@@ -19581,15 +20525,28 @@ function stripAddressComments(address) {
|
|
|
19581
20525
|
continue;
|
|
19582
20526
|
}
|
|
19583
20527
|
if (chr === closer) closer = "";
|
|
19584
|
-
} else if (chr === "\"")
|
|
20528
|
+
} else if (chr === "\"") {
|
|
20529
|
+
closer = "\"";
|
|
20530
|
+
quoteSeen = true;
|
|
20531
|
+
} else if (chr === ":" && routeEnd < 0 && firstChar === "@" && !quoteSeen) routeEnd = result.length + 1;
|
|
19585
20532
|
else if (chr === "[") closer = "]";
|
|
19586
20533
|
else if (chr === "(") {
|
|
19587
20534
|
depth = 1;
|
|
19588
20535
|
continue;
|
|
20536
|
+
} else if (chr === "<") {
|
|
20537
|
+
result = "";
|
|
20538
|
+
lastChar = "";
|
|
20539
|
+
comments.length = 0;
|
|
20540
|
+
firstChar = "";
|
|
20541
|
+
quoteSeen = false;
|
|
20542
|
+
routeEnd = -1;
|
|
20543
|
+
continue;
|
|
19589
20544
|
}
|
|
19590
20545
|
result += chr;
|
|
19591
20546
|
lastChar = chr;
|
|
20547
|
+
if (!firstChar && !isSpaceCode(address.charCodeAt(i))) firstChar = chr;
|
|
19592
20548
|
}
|
|
20549
|
+
if (routeEnd >= 0 && result.indexOf("@", routeEnd) >= 0) result = result.slice(routeEnd);
|
|
19593
20550
|
if (depth) comments.push(comment.trim());
|
|
19594
20551
|
return {
|
|
19595
20552
|
address: result.trim(),
|
|
@@ -19597,62 +20554,36 @@ function stripAddressComments(address) {
|
|
|
19597
20554
|
};
|
|
19598
20555
|
}
|
|
19599
20556
|
/**
|
|
19600
|
-
*
|
|
19601
|
-
*
|
|
19602
|
-
*
|
|
19603
|
-
* Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
|
|
19604
|
-
* followed by a long run of '@' and no closing `>`.
|
|
20557
|
+
* Walks the tokens of one entry of an address list and sorts them into the runs of display
|
|
20558
|
+
* text, angle-addr and comment they belong to
|
|
19605
20559
|
*
|
|
19606
|
-
* @param
|
|
19607
|
-
* @
|
|
20560
|
+
* @param tokens Tokens of the whole address list
|
|
20561
|
+
* @param from Index of the first token of the entry
|
|
20562
|
+
* @param to Index just past the last token of the entry
|
|
20563
|
+
* @return The collected runs, and whether the display text came out of a comment
|
|
19608
20564
|
*/
|
|
19609
|
-
function
|
|
19610
|
-
let start = -1;
|
|
19611
|
-
let at = -1;
|
|
19612
|
-
for (let i = 0; i < text.length; i++) {
|
|
19613
|
-
const chr = text.charAt(i);
|
|
19614
|
-
if (chr === "<") {
|
|
19615
|
-
start = i + 1;
|
|
19616
|
-
at = -1;
|
|
19617
|
-
} else if (chr === ">") {
|
|
19618
|
-
if (at >= 0 && at < i - 1) return true;
|
|
19619
|
-
start = -1;
|
|
19620
|
-
at = -1;
|
|
19621
|
-
} else if (chr === "@" && start >= 0 && at < 0 && i > start) at = i;
|
|
19622
|
-
}
|
|
19623
|
-
return false;
|
|
19624
|
-
}
|
|
19625
|
-
/**
|
|
19626
|
-
* Converts tokens for a single address into an address object
|
|
19627
|
-
*
|
|
19628
|
-
* @param tokens Tokens object
|
|
19629
|
-
* @param depth Current recursion depth for nested group protection
|
|
19630
|
-
* @return Address objects
|
|
19631
|
-
*/
|
|
19632
|
-
function _handleAddress(tokens, depth) {
|
|
19633
|
-
let isGroup = false;
|
|
20565
|
+
function collectAddressParts(tokens, from, to) {
|
|
19634
20566
|
let state = "text";
|
|
19635
|
-
let addresses = [];
|
|
19636
20567
|
let data = {
|
|
19637
20568
|
address: [],
|
|
19638
20569
|
comment: [],
|
|
19639
|
-
group: [],
|
|
19640
20570
|
text: [],
|
|
19641
|
-
textWasQuoted: []
|
|
20571
|
+
textWasQuoted: [],
|
|
20572
|
+
quotedLocalAt: []
|
|
19642
20573
|
};
|
|
20574
|
+
let lastTextPureQuoted = false;
|
|
19643
20575
|
let i;
|
|
19644
|
-
let len;
|
|
19645
20576
|
let insideQuotes = false;
|
|
19646
20577
|
const lastChars = {
|
|
19647
20578
|
address: "",
|
|
19648
20579
|
comment: "",
|
|
19649
|
-
group: "",
|
|
19650
20580
|
text: ""
|
|
19651
20581
|
};
|
|
19652
|
-
for (i =
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
20582
|
+
for (i = from; i < to; i++) {
|
|
20583
|
+
const value = tokens.value(i);
|
|
20584
|
+
const prevOperator = i > from ? tokens.operatorAt(i - 1) : "";
|
|
20585
|
+
const prevNoBreak = i > from && tokens.noBreak(i - 1);
|
|
20586
|
+
if (tokens.isOperator(i)) switch (value) {
|
|
19656
20587
|
case "<":
|
|
19657
20588
|
state = "address";
|
|
19658
20589
|
insideQuotes = false;
|
|
@@ -19661,11 +20592,6 @@ function _handleAddress(tokens, depth) {
|
|
|
19661
20592
|
state = "comment";
|
|
19662
20593
|
insideQuotes = false;
|
|
19663
20594
|
break;
|
|
19664
|
-
case ":":
|
|
19665
|
-
state = "group";
|
|
19666
|
-
isGroup = true;
|
|
19667
|
-
insideQuotes = false;
|
|
19668
|
-
break;
|
|
19669
20595
|
case "\"":
|
|
19670
20596
|
insideQuotes = !insideQuotes;
|
|
19671
20597
|
state = "text";
|
|
@@ -19674,108 +20600,132 @@ function _handleAddress(tokens, depth) {
|
|
|
19674
20600
|
state = "text";
|
|
19675
20601
|
insideQuotes = false;
|
|
19676
20602
|
}
|
|
19677
|
-
else
|
|
19678
|
-
const
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
|
|
19683
|
-
|
|
20603
|
+
else {
|
|
20604
|
+
const opensAfterEmptyQuotedString = prevOperator === "\"" && prevNoBreak && i > from + 1 && tokens.operatorAt(i - 2) === "\"";
|
|
20605
|
+
if (prevNoBreak && data[state].length && (prevOperator !== ")" || lastChars[state] === "@" || value.charAt(0) === "@")) {
|
|
20606
|
+
const last = data[state].length - 1;
|
|
20607
|
+
if (state === "text" && !insideQuotes) {
|
|
20608
|
+
if (lastTextPureQuoted && value.charAt(0) === "@") data.quotedLocalAt[last] = data.text[last].length;
|
|
20609
|
+
lastTextPureQuoted = false;
|
|
20610
|
+
}
|
|
20611
|
+
data[state][last] += value;
|
|
20612
|
+
if (state === "text" && insideQuotes) data.textWasQuoted[last] = true;
|
|
19684
20613
|
} else {
|
|
19685
|
-
data[state].push(
|
|
19686
|
-
if (state === "text")
|
|
20614
|
+
data[state].push(value);
|
|
20615
|
+
if (state === "text") {
|
|
20616
|
+
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
20617
|
+
data.quotedLocalAt.push(opensAfterEmptyQuotedString && value.charAt(0) === "@" ? 0 : -1);
|
|
20618
|
+
lastTextPureQuoted = insideQuotes;
|
|
20619
|
+
}
|
|
19687
20620
|
}
|
|
19688
|
-
lastChars[state] =
|
|
20621
|
+
lastChars[state] = value.charAt(value.length - 1);
|
|
19689
20622
|
}
|
|
19690
20623
|
}
|
|
20624
|
+
let textFromComment = false;
|
|
19691
20625
|
if (!data.text.length && data.comment.length) {
|
|
19692
20626
|
data.text = data.comment;
|
|
19693
20627
|
data.comment = [];
|
|
20628
|
+
textFromComment = true;
|
|
19694
20629
|
}
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
|
|
19706
|
-
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19714
|
-
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
20630
|
+
return {
|
|
20631
|
+
data,
|
|
20632
|
+
textFromComment
|
|
20633
|
+
};
|
|
20634
|
+
}
|
|
20635
|
+
/**
|
|
20636
|
+
* Name of a group, from the tokens in front of its ':'. A comment names it when no text
|
|
20637
|
+
* does, as it names a mailbox.
|
|
20638
|
+
*
|
|
20639
|
+
* @param tokens Tokens of the whole address list
|
|
20640
|
+
* @param from Index of the first token in front of the ':'
|
|
20641
|
+
* @param to Index of the ':'
|
|
20642
|
+
* @return Decoded name of the group
|
|
20643
|
+
*/
|
|
20644
|
+
function groupName(tokens, from, to) {
|
|
20645
|
+
return decodeWords(collectAddressParts(tokens, from, to).data.text.join(" "));
|
|
20646
|
+
}
|
|
20647
|
+
/**
|
|
20648
|
+
* Converts tokens for a single address into an address object
|
|
20649
|
+
*
|
|
20650
|
+
* @param tokens Tokens of the whole address list
|
|
20651
|
+
* @param from Index of the first token of this address
|
|
20652
|
+
* @param to Index just past the last token of this address
|
|
20653
|
+
* @return The address object
|
|
20654
|
+
*/
|
|
20655
|
+
function _handleAddress(tokens, from, to) {
|
|
20656
|
+
const collected = collectAddressParts(tokens, from, to);
|
|
20657
|
+
const data = collected.data;
|
|
20658
|
+
let textFromComment = collected.textFromComment;
|
|
20659
|
+
let i;
|
|
20660
|
+
const addressComments = [];
|
|
20661
|
+
const addressParts = [];
|
|
20662
|
+
for (const part of data.address) {
|
|
20663
|
+
const stripped = stripAddressComments(part);
|
|
20664
|
+
for (const comment of stripped.comments) addressComments.push(comment);
|
|
20665
|
+
if (stripped.address) addressParts.push(stripped.address);
|
|
20666
|
+
}
|
|
20667
|
+
data.address = addressParts;
|
|
20668
|
+
if (!data.address.length && data.text.length && !textFromComment) {
|
|
20669
|
+
const candidates = data.text.map((part) => collapseAtSpacing(part));
|
|
20670
|
+
for (i = 0; i < data.text.length; i++) {
|
|
20671
|
+
const local = data.quotedLocalAt[i];
|
|
20672
|
+
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? quoteLocalPart(data.text[i]) : "";
|
|
20673
|
+
if (quotedLocal || !data.textWasQuoted[i] && isAddrSpec(candidates[i])) {
|
|
20674
|
+
data.address = [quotedLocal || candidates[i]];
|
|
20675
|
+
data.text.splice(i, 1);
|
|
19722
20676
|
data.textWasQuoted.splice(i, 1);
|
|
20677
|
+
data.quotedLocalAt.splice(i, 1);
|
|
19723
20678
|
break;
|
|
19724
20679
|
}
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
20680
|
+
}
|
|
20681
|
+
if (!data.address.length) {
|
|
20682
|
+
for (i = 0; i < data.text.length; i++) if (!data.textWasQuoted[i]) {
|
|
20683
|
+
const part = candidates[i];
|
|
20684
|
+
const start = looseAddressStart(part);
|
|
20685
|
+
if (start >= 0) {
|
|
20686
|
+
LOOSE_TEXT_ADDR.lastIndex = start;
|
|
20687
|
+
const match = LOOSE_TEXT_ADDR.exec(part);
|
|
20688
|
+
if (match) {
|
|
20689
|
+
data.address = [match[0].trim()];
|
|
20690
|
+
data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
|
|
20691
|
+
break;
|
|
19737
20692
|
}
|
|
19738
|
-
data.text[i] = part.trim();
|
|
19739
20693
|
}
|
|
20694
|
+
data.text[i] = data.text[i].trim();
|
|
19740
20695
|
}
|
|
19741
20696
|
}
|
|
19742
|
-
if (!data.text.length && data.comment.length) {
|
|
19743
|
-
data.text = data.comment;
|
|
19744
|
-
data.comment = [];
|
|
19745
|
-
}
|
|
19746
|
-
if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
|
|
19747
|
-
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
|
|
19748
|
-
let text = data.text.join(" ");
|
|
19749
|
-
let addressValue = data.address.join(" ");
|
|
19750
|
-
if (!addressValue && isEncodedWordsOnly(text.trim())) {
|
|
19751
|
-
const decodedText = decodeWords(text);
|
|
19752
|
-
if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
|
|
19753
|
-
const parsedSubAddresses = parseAddressList$1(decodedText, depth + 1);
|
|
19754
|
-
if (parsedSubAddresses.length) return parsedSubAddresses;
|
|
19755
|
-
}
|
|
19756
|
-
return [{
|
|
19757
|
-
address: "",
|
|
19758
|
-
name: decodedText
|
|
19759
|
-
}];
|
|
19760
|
-
}
|
|
19761
|
-
if (addressFromQuotedText && text.indexOf("@") >= 0) {
|
|
19762
|
-
addressValue = quoteLocalPart(text);
|
|
19763
|
-
text = "";
|
|
19764
|
-
}
|
|
19765
|
-
({address: addressValue, text} = recoverAddrSpec(addressValue, text));
|
|
19766
|
-
if (!text && addressComments.length) text = addressComments.join(" ");
|
|
19767
|
-
const rawName = text || addressValue || "";
|
|
19768
|
-
const address = {
|
|
19769
|
-
address: addressValue || text || "",
|
|
19770
|
-
name: decodeWords(rawName)
|
|
19771
|
-
};
|
|
19772
|
-
if (address.address === rawName || address.address === address.name) {
|
|
19773
|
-
if (address.address.match(/@/)) address.name = "";
|
|
19774
|
-
else address.address = "";
|
|
19775
|
-
}
|
|
19776
|
-
addresses.push(address);
|
|
19777
20697
|
}
|
|
19778
|
-
|
|
20698
|
+
if (!data.text.length && data.comment.length) {
|
|
20699
|
+
data.text = data.comment;
|
|
20700
|
+
data.comment = [];
|
|
20701
|
+
}
|
|
20702
|
+
if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
|
|
20703
|
+
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
|
|
20704
|
+
let text = data.text.join(" ");
|
|
20705
|
+
let addressValue = data.address.join(" ");
|
|
20706
|
+
if (!addressValue && isEncodedWordsOnly(text.trim())) return {
|
|
20707
|
+
address: "",
|
|
20708
|
+
name: decodeWords(text)
|
|
20709
|
+
};
|
|
20710
|
+
if (addressFromQuotedText && text.indexOf("@") >= 0) {
|
|
20711
|
+
addressValue = quoteLocalPart(joinAtSpacing(text, domainAt(text)));
|
|
20712
|
+
text = "";
|
|
20713
|
+
}
|
|
20714
|
+
({address: addressValue, text} = recoverAddrSpec(addressValue, text));
|
|
20715
|
+
if (!text && addressComments.length) {
|
|
20716
|
+
text = addressComments.join(" ");
|
|
20717
|
+
textFromComment = true;
|
|
20718
|
+
}
|
|
20719
|
+
const rawName = text || addressValue || "";
|
|
20720
|
+
const address = {
|
|
20721
|
+
address: addressValue || (textFromComment ? "" : text) || "",
|
|
20722
|
+
name: decodeWords(rawName)
|
|
20723
|
+
};
|
|
20724
|
+
if (address.address === rawName || address.address === address.name) {
|
|
20725
|
+
if (address.address.match(/@/)) address.name = "";
|
|
20726
|
+
else address.address = "";
|
|
20727
|
+
}
|
|
20728
|
+
return address;
|
|
19779
20729
|
}
|
|
19780
20730
|
/**
|
|
19781
20731
|
* Tokenizer for address field strings
|
|
@@ -19788,17 +20738,22 @@ var Tokenizer$2 = class {
|
|
|
19788
20738
|
this.str = (str || "").toString();
|
|
19789
20739
|
this.operatorCurrent = "";
|
|
19790
20740
|
this.operatorExpecting = "";
|
|
19791
|
-
this.node =
|
|
20741
|
+
this.node = -1;
|
|
19792
20742
|
this.escaped = false;
|
|
19793
20743
|
this.inDomainLiteral = false;
|
|
19794
20744
|
this.afterAt = false;
|
|
19795
|
-
this.
|
|
20745
|
+
this.inGroup = false;
|
|
20746
|
+
this.innerQuoted = false;
|
|
20747
|
+
this.innerComment = 0;
|
|
20748
|
+
this.innerLiteral = false;
|
|
20749
|
+
this.innerEscaped = false;
|
|
20750
|
+
this.list = new TokenList(this.str);
|
|
19796
20751
|
this.operators = {
|
|
19797
20752
|
"\"": "\"",
|
|
19798
20753
|
"(": ")",
|
|
19799
20754
|
"<": ">",
|
|
19800
20755
|
",": "",
|
|
19801
|
-
":": "
|
|
20756
|
+
":": "",
|
|
19802
20757
|
";": ""
|
|
19803
20758
|
};
|
|
19804
20759
|
}
|
|
@@ -19808,115 +20763,259 @@ var Tokenizer$2 = class {
|
|
|
19808
20763
|
* @return An array of operator|text tokens
|
|
19809
20764
|
*/
|
|
19810
20765
|
tokenize() {
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
20766
|
+
const str = this.str;
|
|
20767
|
+
const len = str.length;
|
|
20768
|
+
for (let i = 0; i < len; i++) this.checkChar(str.charAt(i), i);
|
|
20769
|
+
this.closeText(len);
|
|
20770
|
+
const list = this.list;
|
|
20771
|
+
const slots = list.slots;
|
|
20772
|
+
let kept = 0;
|
|
20773
|
+
for (let i = 0, count = list.length; i < count; i++) {
|
|
20774
|
+
const kind = slots[i * 3];
|
|
20775
|
+
let start = slots[i * 3 + 1];
|
|
20776
|
+
let end = slots[i * 3 + 2];
|
|
20777
|
+
if (kind & TOKEN_DIRTY) {
|
|
20778
|
+
const value = this.cleanValue(start, end, kind).trim();
|
|
20779
|
+
if (!value) continue;
|
|
20780
|
+
list.cleaned.set(kept, value);
|
|
20781
|
+
} else if (!(kind & TOKEN_OPERATOR)) {
|
|
20782
|
+
while (start < end && isSpaceCode(str.charCodeAt(start))) start++;
|
|
20783
|
+
while (end > start && isSpaceCode(str.charCodeAt(end - 1))) end--;
|
|
20784
|
+
if (start === end) continue;
|
|
20785
|
+
}
|
|
20786
|
+
slots[kept * 3] = kind;
|
|
20787
|
+
slots[kept * 3 + 1] = start;
|
|
20788
|
+
slots[kept * 3 + 2] = end;
|
|
20789
|
+
kept++;
|
|
20790
|
+
}
|
|
20791
|
+
list.length = kept;
|
|
19821
20792
|
return list;
|
|
19822
20793
|
}
|
|
19823
20794
|
/**
|
|
20795
|
+
* Ends the text run being collected, if there is one
|
|
20796
|
+
*
|
|
20797
|
+
* @param end Offset just past its last character
|
|
20798
|
+
*/
|
|
20799
|
+
closeText(end) {
|
|
20800
|
+
if (this.node >= 0) {
|
|
20801
|
+
this.list.slots[this.node * 3 + 2] = end;
|
|
20802
|
+
this.node = -1;
|
|
20803
|
+
}
|
|
20804
|
+
}
|
|
20805
|
+
/**
|
|
20806
|
+
* Rebuilds the value of a text run that is no plain slice of the field, applying the
|
|
20807
|
+
* rules checkChar collects characters by: an escaping backslash is dropped, a line break
|
|
20808
|
+
* reads as a space and any other control character is left out
|
|
20809
|
+
*
|
|
20810
|
+
* @param start Offset of the first character of the run
|
|
20811
|
+
* @param end Offset just past its last character
|
|
20812
|
+
* @param kind Kind and flags of the run
|
|
20813
|
+
* @return Value of the run
|
|
20814
|
+
*/
|
|
20815
|
+
cleanValue(start, end, kind) {
|
|
20816
|
+
const str = this.str;
|
|
20817
|
+
const escapes = (kind & TOKEN_ESCAPES) !== 0;
|
|
20818
|
+
let escaped = (kind & TOKEN_STARTS_ESCAPED) !== 0;
|
|
20819
|
+
let value = "";
|
|
20820
|
+
for (let i = start; i < end; i++) {
|
|
20821
|
+
const chr = str.charAt(i);
|
|
20822
|
+
if (escapes && !escaped && chr === "\\") {
|
|
20823
|
+
escaped = true;
|
|
20824
|
+
continue;
|
|
20825
|
+
}
|
|
20826
|
+
escaped = false;
|
|
20827
|
+
if (chr === "\n") value += " ";
|
|
20828
|
+
else if (chr.charCodeAt(0) >= 33 || chr === " " || chr === " ") value += chr;
|
|
20829
|
+
}
|
|
20830
|
+
return value;
|
|
20831
|
+
}
|
|
20832
|
+
/**
|
|
19824
20833
|
* Checks if a character is an operator or text and acts accordingly
|
|
19825
20834
|
*
|
|
19826
20835
|
* @param chr Character from the address field
|
|
19827
|
-
* @param
|
|
20836
|
+
* @param i Offset of the character in the field
|
|
19828
20837
|
*/
|
|
19829
|
-
checkChar(chr,
|
|
20838
|
+
checkChar(chr, i) {
|
|
19830
20839
|
if (!this.escaped && !this.operatorExpecting) {
|
|
19831
20840
|
if (!this.inDomainLiteral && chr === "[" && this.afterAt) this.inDomainLiteral = true;
|
|
19832
20841
|
else if (this.inDomainLiteral && (chr === "]" || chr === "," || chr === ";")) this.inDomainLiteral = false;
|
|
19833
20842
|
}
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
20843
|
+
let nested = false;
|
|
20844
|
+
if (this.operatorExpecting === ">") {
|
|
20845
|
+
nested = this.trackInner(chr);
|
|
20846
|
+
if (!nested && chr === ";" && this.inGroup) {
|
|
20847
|
+
this.closeText(i);
|
|
20848
|
+
this.operatorExpecting = "";
|
|
20849
|
+
this.resetInner();
|
|
20850
|
+
}
|
|
20851
|
+
} else if (this.operatorExpecting === ")" && !this.escaped) {
|
|
20852
|
+
if (chr === "(") this.innerComment++;
|
|
20853
|
+
else if (chr === ")" && this.innerComment) {
|
|
20854
|
+
this.innerComment--;
|
|
20855
|
+
nested = true;
|
|
20856
|
+
}
|
|
20857
|
+
}
|
|
20858
|
+
if (this.escaped) {} else if (chr === this.operatorExpecting && !nested) {
|
|
20859
|
+
const nextChr = this.str.charAt(i + 1);
|
|
20860
|
+
this.closeText(i);
|
|
20861
|
+
this.list.push(nextChr && ![
|
|
19840
20862
|
" ",
|
|
19841
20863
|
" ",
|
|
19842
20864
|
"\r",
|
|
19843
20865
|
"\n",
|
|
19844
20866
|
",",
|
|
19845
20867
|
";"
|
|
19846
|
-
].includes(nextChr)
|
|
19847
|
-
this.list.push(this.node);
|
|
19848
|
-
this.node = null;
|
|
20868
|
+
].includes(nextChr) ? 3 : TOKEN_OPERATOR, i, i + 1);
|
|
19849
20869
|
if (chr !== ")") this.afterAt = false;
|
|
19850
20870
|
this.operatorExpecting = "";
|
|
19851
20871
|
this.escaped = false;
|
|
20872
|
+
this.resetInner();
|
|
19852
20873
|
return;
|
|
19853
20874
|
} else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ":") && chr in this.operators) {
|
|
19854
|
-
this.
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
this.list.push(this.node);
|
|
19859
|
-
this.node = null;
|
|
20875
|
+
this.closeText(i);
|
|
20876
|
+
this.list.push(TOKEN_OPERATOR, i, i + 1);
|
|
20877
|
+
if (chr === ":") this.inGroup = true;
|
|
20878
|
+
else if (chr === ";") this.inGroup = false;
|
|
19860
20879
|
if (chr !== "(") this.afterAt = false;
|
|
19861
20880
|
this.operatorExpecting = this.operators[chr];
|
|
19862
20881
|
this.escaped = false;
|
|
20882
|
+
this.resetInner();
|
|
19863
20883
|
return;
|
|
19864
20884
|
} else if ((this.operatorExpecting === "\"" || this.operatorExpecting === ")") && chr === "\\") {
|
|
19865
20885
|
this.escaped = true;
|
|
20886
|
+
if (this.node >= 0) this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
19866
20887
|
return;
|
|
19867
20888
|
}
|
|
19868
|
-
if (
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
};
|
|
19873
|
-
this.list.push(this.node);
|
|
20889
|
+
if (this.node < 0) this.node = this.list.push((this.operatorExpecting === "\"" || this.operatorExpecting === ")" ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
20890
|
+
if (chr === "\n") {
|
|
20891
|
+
chr = " ";
|
|
20892
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
19874
20893
|
}
|
|
19875
|
-
if (chr === "
|
|
19876
|
-
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
|
|
19877
|
-
this.node.value += chr;
|
|
20894
|
+
if (chr.charCodeAt(0) >= 33 || chr === " " || chr === " ") {
|
|
19878
20895
|
if (!this.operatorExpecting && chr !== " " && chr !== " ") this.afterAt = chr === "@";
|
|
19879
|
-
}
|
|
20896
|
+
} else this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
19880
20897
|
this.escaped = false;
|
|
19881
20898
|
}
|
|
20899
|
+
/**
|
|
20900
|
+
* Tracks the quoted strings, comments and domain literals inside an angle-addr, which its
|
|
20901
|
+
* closer must not be read from. A backslash escapes the next character inside a quoted
|
|
20902
|
+
* string and a comment. A domain literal opens right after an "@" and ends at the "]".
|
|
20903
|
+
* dtext holds the list separators as well, and inside the brackets they never split the
|
|
20904
|
+
* list anyway.
|
|
20905
|
+
*
|
|
20906
|
+
* @param chr Character from the address field
|
|
20907
|
+
* @return true while the character sits inside one of them, the one that closes it included
|
|
20908
|
+
*/
|
|
20909
|
+
trackInner(chr) {
|
|
20910
|
+
if (this.innerEscaped) {
|
|
20911
|
+
this.innerEscaped = false;
|
|
20912
|
+
return true;
|
|
20913
|
+
}
|
|
20914
|
+
if (this.innerQuoted) {
|
|
20915
|
+
if (chr === "\\") this.innerEscaped = true;
|
|
20916
|
+
else if (chr === "\"") this.innerQuoted = false;
|
|
20917
|
+
return true;
|
|
20918
|
+
}
|
|
20919
|
+
if (this.innerComment) {
|
|
20920
|
+
if (chr === "\\") this.innerEscaped = true;
|
|
20921
|
+
else if (chr === "(") this.innerComment++;
|
|
20922
|
+
else if (chr === ")") this.innerComment--;
|
|
20923
|
+
return true;
|
|
20924
|
+
}
|
|
20925
|
+
if (this.innerLiteral) {
|
|
20926
|
+
if (chr === "]") this.innerLiteral = false;
|
|
20927
|
+
return true;
|
|
20928
|
+
}
|
|
20929
|
+
if (chr === "\"") {
|
|
20930
|
+
this.innerQuoted = true;
|
|
20931
|
+
return true;
|
|
20932
|
+
}
|
|
20933
|
+
if (chr === "(") {
|
|
20934
|
+
this.innerComment = 1;
|
|
20935
|
+
return true;
|
|
20936
|
+
}
|
|
20937
|
+
if (chr === "[" && this.afterAt) {
|
|
20938
|
+
this.innerLiteral = true;
|
|
20939
|
+
return true;
|
|
20940
|
+
}
|
|
20941
|
+
if (!isSpaceCode(chr.charCodeAt(0))) this.afterAt = chr === "@";
|
|
20942
|
+
return false;
|
|
20943
|
+
}
|
|
20944
|
+
/**
|
|
20945
|
+
* Forgets the nesting collected for the operator that just opened or ended
|
|
20946
|
+
*/
|
|
20947
|
+
resetInner() {
|
|
20948
|
+
this.innerQuoted = false;
|
|
20949
|
+
this.innerComment = 0;
|
|
20950
|
+
this.innerLiteral = false;
|
|
20951
|
+
this.innerEscaped = false;
|
|
20952
|
+
}
|
|
19882
20953
|
};
|
|
19883
20954
|
/**
|
|
19884
|
-
*
|
|
19885
|
-
*
|
|
19886
|
-
*
|
|
20955
|
+
* Folds the fragments an unquoted display name holding a comma is split into back together.
|
|
20956
|
+
*
|
|
20957
|
+
* "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
20958
|
+
* [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}]. A name-only entry
|
|
20959
|
+
* followed by an entry with both name and address is recombined. Walked back to front so
|
|
20960
|
+
* that a run of fragments folds into one entry in a single pass. Splicing each fragment out
|
|
20961
|
+
* of the list instead would be quadratic.
|
|
20962
|
+
*
|
|
20963
|
+
* @param list Parsed entries, in order
|
|
20964
|
+
* @return The entries with the fragments folded in
|
|
19887
20965
|
*/
|
|
19888
|
-
|
|
20966
|
+
function mergeNameFragments(list) {
|
|
20967
|
+
const merged = [];
|
|
20968
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
20969
|
+
const current = list[i];
|
|
20970
|
+
const next = merged.length ? merged[merged.length - 1] : null;
|
|
20971
|
+
if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
|
|
20972
|
+
else merged.push(current);
|
|
20973
|
+
}
|
|
20974
|
+
return merged.reverse();
|
|
20975
|
+
}
|
|
19889
20976
|
/**
|
|
19890
|
-
* Parses an address list,
|
|
19891
|
-
*
|
|
19892
|
-
*
|
|
20977
|
+
* Parses an address list, groups included, in a single pass over its tokens.
|
|
20978
|
+
*
|
|
20979
|
+
* A group used to be cut out of the field at the ';' that ends it and parsed again as an
|
|
20980
|
+
* address list of its own, recursively, so every level of nesting parsed what was left of
|
|
20981
|
+
* the field once more. RFC 5322 does not nest groups at all, and the nested ones were
|
|
20982
|
+
* flattened anyway. The tokens are now walked once: a ':' opens a group, its members are
|
|
20983
|
+
* read as they come, a ';' closes it. A ':' inside an open group stands for a group nested
|
|
20984
|
+
* in it, whose name is dropped and whose members belong to the outer one, and the first ';'
|
|
20985
|
+
* ends the group, nested or not, as before.
|
|
19893
20986
|
*
|
|
19894
20987
|
* @param str Address field
|
|
19895
|
-
* @param depth Current recursion depth for nested group protection
|
|
19896
20988
|
* @return An array of address objects
|
|
19897
20989
|
*/
|
|
19898
|
-
function parseAddressList$1(str
|
|
19899
|
-
|
|
19900
|
-
let tokens = new Tokenizer$2(str).tokenize();
|
|
19901
|
-
let addresses = [];
|
|
19902
|
-
let address = [];
|
|
20990
|
+
function parseAddressList$1(str) {
|
|
20991
|
+
const tokens = new Tokenizer$2(str).tokenize();
|
|
19903
20992
|
const parsedAddresses = [];
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
20993
|
+
let group = null;
|
|
20994
|
+
let members = [];
|
|
20995
|
+
let from = 0;
|
|
20996
|
+
for (let i = 0, len = tokens.length; i <= len; i++) {
|
|
20997
|
+
const operator = i < len ? tokens.operatorAt(i) : "";
|
|
20998
|
+
if (i < len && operator !== "," && operator !== ";" && operator !== ":") continue;
|
|
20999
|
+
if (operator === ":") {
|
|
21000
|
+
if (!group) {
|
|
21001
|
+
group = {
|
|
21002
|
+
name: groupName(tokens, from, i),
|
|
21003
|
+
group: []
|
|
21004
|
+
};
|
|
21005
|
+
members = [];
|
|
21006
|
+
}
|
|
21007
|
+
from = i + 1;
|
|
21008
|
+
continue;
|
|
21009
|
+
}
|
|
21010
|
+
if (i > from) (group ? members : parsedAddresses).push(_handleAddress(tokens, from, i));
|
|
21011
|
+
from = i + 1;
|
|
21012
|
+
if (group && (operator === ";" || i === len)) {
|
|
21013
|
+
group.group = mergeNameFragments(members);
|
|
21014
|
+
parsedAddresses.push(group);
|
|
21015
|
+
group = null;
|
|
21016
|
+
}
|
|
19918
21017
|
}
|
|
19919
|
-
return
|
|
21018
|
+
return mergeNameFragments(parsedAddresses);
|
|
19920
21019
|
}
|
|
19921
21020
|
/**
|
|
19922
21021
|
* Parses structured e-mail addresses from an address field
|
|
@@ -19934,7 +21033,7 @@ function parseAddressList$1(str, depth) {
|
|
|
19934
21033
|
* @return An array of address objects
|
|
19935
21034
|
*/
|
|
19936
21035
|
function addressParser(str, options) {
|
|
19937
|
-
const parsedAddresses = parseAddressList$1(str
|
|
21036
|
+
const parsedAddresses = parseAddressList$1(str);
|
|
19938
21037
|
if (options && options.flatten) {
|
|
19939
21038
|
let addresses = [];
|
|
19940
21039
|
let walkAddressList = (list) => {
|