@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.
@@ -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-USuVU_sk.mjs";
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.25.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-CMD_Te5S.mjs
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-l0mj9_dH.mjs");
5587
- const { StdioClientTransport } = await import("./stdio-CyjIgrch-59zcjSEt.mjs");
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.25.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$1(value) {
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$1(before ?? {}), recordOf$1(after ?? {})];
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$1(before ?? {}), recordOf$1(after ?? {})];
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$2 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
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$2(entry.at)) return null;
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$2(entry.actAt)) return null;
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$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
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$1(record.lastRun) ? { lastRun: record.lastRun } : {}
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$1(at)) return false;
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
- }, { timeoutMs: 2e3 });
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, and a decision it needs from
16526
- * the person — as flags only. The decision's own words are never read out: they are the registry's, and an update says
16527
- * that one is needed in its own words. A declaration this release cannot read waits for the person: it may be the
16528
- * decision it cannot see.
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
- * Every release the registry lists after `from`, up to and including `to`, oldest first: what an update takes this
16548
- * machine past. Prereleases are left out — no update installs one — and so is anything that is not a version, or that
16549
- * carries build metadata, which no published version does.
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 releasesBetween(packument, from, to) {
16552
- return Object.keys(packument.versions ?? {}).filter((version) => isVersion(version) && !isPrerelease(version) && !version.includes("+") && (compareVersions(version, from) ?? 0) > 0 && (compareVersions(version, to) ?? 1) <= 0).sort((a, b) => compareVersions(a, b) ?? 0);
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.25.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 version = installed[name];
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 — or null, where it is not to be read
17677
- * or could not be: never a reason for the check to fail. Read only for a release newer than nothing — a counted one.
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 null;
17682
- const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps);
17683
- if (read === null) return null;
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
- return {
18292
+ const facts = await read(CORE_PACKAGE$1, latest, oldestHere(report));
18293
+ return facts === null ? null : {
17686
18294
  version: latest,
17687
- ...await read(CORE_PACKAGE$1, latest, oldestHere(report))
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 every release after it is read for: the core
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 releases declare (`declaredSince`): the registry handed in,
17702
- * or the one npm reads — but only beside the registry's own `latest`: a test with a stand-in `latestVersion` hands in
17703
- * `releaseFacts` itself, or none is read. With the verifier handed in, and none where none was: a channel's process
17704
- * carries no Sigstore, so what it reads counts for nothing. Each package's document is read once, however often asked.
17705
- */
17706
- function releaseFactsReader(env, deps) {
17707
- if (deps.latestVersion !== void 0) return null;
17708
- const read = deps.registry ?? npmRegistry(env);
17709
- const documents = /* @__PURE__ */ new Map();
17710
- const registry = (packageName) => {
17711
- const known = documents.get(packageName) ?? read(packageName);
17712
- documents.set(packageName, known);
17713
- return known;
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 { security, needsDecision } = await declaredSince(registry, deps.verifyRelease ?? null, packageName, from, version);
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 !== void 0 ? release : record.release?.version === latest ? record.release : null;
17763
- await changeUpdateCheck(core.paths.stateDir, (record) => claim !== void 0 && record.checking !== claim ? null : { record: {
17764
- ...record,
17765
- lastChecked: at,
17766
- latest,
17767
- behind,
17768
- current,
17769
- lastError: null,
17770
- checking: null,
17771
- release: facts(record),
17772
- moves
17773
- } }).catch(() => void 0);
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
- return (await changeUpdateCheck(options.core.paths.stateDir, (record) => {
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
- })).result ?? null;
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 ? null : await read(CORE_PACKAGE, latest, VERSION).then((facts) => ({
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
- }), () => null);
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
- * When the child cannot be started — no entry to start, a program not there or not allowed — the check is asked in
18506
- * this process instead, as it was before. It never throws.
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 startUpdateCheckChild(entry, claimedAt, core.paths, env, deadline))) await askUnderClaim(core, env, claimedAt, {
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
- * Starts the child and waits, until `deadline` at most, for it to say the ask is over, or to end. False when it could
18527
- * not be started at all; then nothing was asked, and the claim is still this process's to ask under.
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
- async function startUpdateCheckChild(entry, claimedAt, paths, env, deadline) {
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
- const pathArgs = PATH_OPTIONS.flatMap(({ key, flag }) => [flag, paths[key]]);
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: updateCheckChildEnvironment(env)
19264
+ env
18548
19265
  });
18549
19266
  } catch {
18550
19267
  return false;
18551
19268
  }
18552
- let failed = false;
18553
- child.on("error", () => {
18554
- if (child.pid === void 0) failed = true;
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 !failed;
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.verifyRelease ? { deps: { verifyRelease: options.verifyRelease } } : {});
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-BW3WUyxT.mjs
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.0.5/node_modules/postal-mime/dist/esm/decode-strings.js
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(charset) {
19110
- if (!ENCODING_LABELS.has(charset)) return null;
19111
- let decoder = decoders.get(charset);
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(charset);
19872
+ decoder = new TextDecoder(label);
19116
19873
  } catch {}
19117
- decoders.set(charset, decoder);
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
- charset = (charset || "utf8").trim().toLowerCase();
19127
- const decoder = tryDecoder(charset);
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 decoder = findDecoder(charset);
19190
- return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
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 && !/=$/.test(left.encodedText);
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.0.5/node_modules/postal-mime/dist/esm/address-parser.js
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.lastIndexOf("@");
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((part) => ADDR_SPEC.test(part));
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
- if (lastChar !== "@" && address.charAt(i + 1) !== "@") {
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 === "\"") closer = "\"";
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
- * Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
19601
- * without other angle brackets that has an '@' with at least one character on each side.
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 text Text to check
19607
- * @return true if an angle bracket address is present
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 hasAngleAddress(text) {
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 = 0, len = tokens.length; i < len; i++) {
19653
- let token = tokens[i];
19654
- let prevToken = i ? tokens[i - 1] : null;
19655
- if (token.type === "operator") switch (token.value) {
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 if (token.value) {
19678
- const prevPrevToken = i > 1 ? tokens[i - 2] : null;
19679
- const opensAfterEmptyQuotedString = prevToken?.type === "operator" && prevToken.value === "\"" && !!prevToken.noBreak && prevPrevToken?.type === "operator" && prevPrevToken.value === "\"";
19680
- if (state === "address") token.value = token.value.replace(/^[^<]*<\s*/, "");
19681
- if (prevToken && prevToken.noBreak && data[state].length && (prevToken.value !== ")" || lastChars[state] === "@" || token.value.charAt(0) === "@")) {
19682
- data[state][data[state].length - 1] += token.value;
19683
- if (state === "text" && insideQuotes) data.textWasQuoted[data.textWasQuoted.length - 1] = true;
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(token.value);
19686
- if (state === "text") data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
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] = token.value.charAt(token.value.length - 1);
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
- if (isGroup) {
19696
- const text = data.text.join(" ");
19697
- let groupMembers = [];
19698
- if (data.group.length) parseAddressList$1(data.group.join(","), depth + 1).forEach((member) => {
19699
- if (member.group) for (const nestedMember of member.group) groupMembers.push(nestedMember);
19700
- else groupMembers.push(member);
19701
- });
19702
- addresses.push({
19703
- name: decodeWords(text),
19704
- group: groupMembers
19705
- });
19706
- } else {
19707
- const addressComments = [];
19708
- const addressParts = [];
19709
- for (const part of data.address) {
19710
- if (part.indexOf("(") < 0) {
19711
- addressParts.push(part);
19712
- continue;
19713
- }
19714
- const stripped = stripAddressComments(part);
19715
- for (const comment of stripped.comments) addressComments.push(comment);
19716
- if (stripped.address) addressParts.push(stripped.address);
19717
- }
19718
- data.address = addressParts;
19719
- if (!data.address.length && data.text.length) {
19720
- for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
19721
- data.address = data.text.splice(i, 1);
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
- if (!data.address.length) {
19726
- for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i]) {
19727
- const part = data.text[i];
19728
- const start = looseAddressStart(part);
19729
- if (start >= 0) {
19730
- LOOSE_TEXT_ADDR.lastIndex = start;
19731
- const match = LOOSE_TEXT_ADDR.exec(part);
19732
- if (match) {
19733
- data.address = [match[0].trim()];
19734
- data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
19735
- break;
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
- return addresses;
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 = null;
20741
+ this.node = -1;
19792
20742
  this.escaped = false;
19793
20743
  this.inDomainLiteral = false;
19794
20744
  this.afterAt = false;
19795
- this.list = [];
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
- let list = [];
19812
- for (let i = 0, len = this.str.length; i < len; i++) {
19813
- let chr = this.str.charAt(i);
19814
- let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
19815
- this.checkChar(chr, nextChr);
19816
- }
19817
- this.list.forEach((node) => {
19818
- node.value = (node.value || "").toString().trim();
19819
- if (node.value) list.push(node);
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 nextChr The character that follows, or null at the end of the field
20836
+ * @param i Offset of the character in the field
19828
20837
  */
19829
- checkChar(chr, nextChr) {
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
- if (this.escaped) {} else if (chr === this.operatorExpecting) {
19835
- this.node = {
19836
- type: "operator",
19837
- value: chr
19838
- };
19839
- if (nextChr && ![
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)) this.node.noBreak = true;
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.node = {
19855
- type: "operator",
19856
- value: chr
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 (!this.node) {
19869
- this.node = {
19870
- type: "text",
19871
- value: ""
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 === "\n") 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
- * Maximum recursion depth for parsing nested groups.
19885
- * RFC 5322 doesn't allow nested groups, so this is a safeguard against
19886
- * malicious input that could cause stack overflow.
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
- const MAX_NESTED_GROUP_DEPTH = 50;
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, recursing into groups. The depth is threaded through the
19891
- * calls rather than taken from an option, so a caller supplied options object can not
19892
- * seed it and lift the recursion limit.
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, depth) {
19899
- if (depth > MAX_NESTED_GROUP_DEPTH) return [];
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
- tokens.forEach((token) => {
19905
- if (token.type === "operator" && (token.value === "," || token.value === ";")) {
19906
- if (address.length) addresses.push(address);
19907
- address = [];
19908
- } else address.push(token);
19909
- });
19910
- if (address.length) addresses.push(address);
19911
- for (const tokens of addresses) for (const address of _handleAddress(tokens, depth)) parsedAddresses.push(address);
19912
- const mergedAddresses = [];
19913
- for (let i = parsedAddresses.length - 1; i >= 0; i--) {
19914
- const current = parsedAddresses[i];
19915
- const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
19916
- if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
19917
- else mergedAddresses.push(current);
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 mergedAddresses.reverse();
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, 0);
21036
+ const parsedAddresses = parseAddressList$1(str);
19938
21037
  if (options && options.flatten) {
19939
21038
  let addresses = [];
19940
21039
  let walkAddressList = (list) => {