@claudinho/core 0.8.18 → 0.8.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -183,6 +183,54 @@ declare function isValidTimeZone(tz: string | undefined | null): boolean;
183
183
  */
184
184
  declare function isValidDate(s: string | undefined | null): boolean;
185
185
 
186
+ /**
187
+ * Feed-string sanitizer — the chokepoint between untrusted provider data and
188
+ * every output surface (terminal, statusline, share cards, and the Claude Code
189
+ * hook, whose stdout lands in the model's context). Strips control characters
190
+ * (C0 incl. ESC, DEL, C1) so a compromised feed can't inject ANSI escapes or
191
+ * multi-line text, and caps length so one field can't flood a surface.
192
+ *
193
+ * Applied at the ESPN adapter boundary (toTeam / mapEspnEvent) and mirrored on
194
+ * the statusline's cache reads (defense against a poisoned cache file).
195
+ */
196
+
197
+ /** Default per-field cap — generous for any real team/venue name. */
198
+ declare const FEED_TEXT_MAX = 100;
199
+ /**
200
+ * Strip C0/C1 control characters (including ESC) and cap at `max` code points.
201
+ * Whitespace controls (tab/newline/CR) become a single space so words a hostile
202
+ * feed split across lines don't fuse together. Total: never throws.
203
+ */
204
+ declare function sanitizeFeedText(value: string, max?: number): string;
205
+ /**
206
+ * Sanitized, display-safe copy of a Match. Used on cache reads (the
207
+ * statusline/hook render straight from the cache file), so it must be total:
208
+ * a malformed entry yields empty strings, never a throw. Beyond the string
209
+ * fields, the RENDERED numeric fields (score, shootout, minute) are dropped
210
+ * unless they are real finite numbers — poisoned values degrade to "vs" /
211
+ * "LIVE", never to injected text. Shootout never survives without its score
212
+ * (the adapter-level invariant, re-enforced here).
213
+ */
214
+ declare function sanitizeMatchStrings(m: Match): Match;
215
+
216
+ /**
217
+ * Display-width helpers for monospace/plain-text alignment.
218
+ *
219
+ * `String.prototype.padEnd` counts UTF-16 units, but emoji flags occupy 2
220
+ * terminal columns regardless of how many units encode them: a regional-
221
+ * indicator flag (🇲🇽) is 4 units, while a tag-sequence flag (England 🏴󠁧󠁢󠁥󠁮󠁧󠁿) is
222
+ * 14 — so unit-counted padding pushed England/Scotland rows ~10 columns out of
223
+ * line. These helpers count grapheme clusters, with pictographic clusters
224
+ * (flags included) as 2 columns, matching how terminals render them.
225
+ */
226
+ /** Terminal display width of a string (grapheme clusters; emoji count as 2). */
227
+ declare function displayWidth(s: string): number;
228
+ /**
229
+ * Pad with trailing spaces to `width` DISPLAY columns (never truncates — a
230
+ * too-long value overflows its column rather than being cut mid-name).
231
+ */
232
+ declare function padVisible(s: string, width: number): string;
233
+
186
234
  /** Outcome (home perspective) from a final/looking scoreline. */
187
235
  declare function outcomeFromScore(home: number, away: number): Outcome;
188
236
  /** Is the match currently in play (including halftime)? */
@@ -1176,4 +1224,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
1176
1224
  /** Compact one-line-per-match bracket for narrow share contexts. */
1177
1225
  declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
1178
1226
 
1179
- export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, scoreline, stageLabel, stageLabelI18n, t };
1227
+ export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
package/dist/index.js CHANGED
@@ -502,6 +502,67 @@ function shiftUtcDate(dateISO, days) {
502
502
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
503
503
  }
504
504
 
505
+ // src/sanitize.ts
506
+ var FEED_TEXT_MAX = 100;
507
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
508
+ let out = "";
509
+ let count = 0;
510
+ for (const ch of String(value)) {
511
+ const cp = ch.codePointAt(0) ?? 0;
512
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
513
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
514
+ if (count >= max) break;
515
+ out += isWhitespaceControl ? " " : ch;
516
+ count++;
517
+ }
518
+ return out;
519
+ }
520
+ function sanitizeTeam(t2) {
521
+ return {
522
+ ...t2 ?? {},
523
+ code: sanitizeFeedText(t2?.code ?? ""),
524
+ name: sanitizeFeedText(t2?.name ?? ""),
525
+ flag: sanitizeFeedText(t2?.flag ?? "")
526
+ };
527
+ }
528
+ function finiteOrUndefined(v) {
529
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
530
+ }
531
+ function sanitizeScorePair(v) {
532
+ const home = finiteOrUndefined(v?.home);
533
+ const away = finiteOrUndefined(v?.away);
534
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
535
+ }
536
+ function sanitizeMatchStrings(m) {
537
+ const score = sanitizeScorePair(m.score);
538
+ return {
539
+ ...m,
540
+ venue: sanitizeFeedText(m.venue ?? ""),
541
+ city: m.city == null ? m.city : sanitizeFeedText(m.city),
542
+ country: m.country == null ? m.country : sanitizeFeedText(m.country),
543
+ home: sanitizeTeam(m.home),
544
+ away: sanitizeTeam(m.away),
545
+ score,
546
+ shootout: score ? sanitizeScorePair(m.shootout) : void 0,
547
+ minute: finiteOrUndefined(m.minute)
548
+ };
549
+ }
550
+
551
+ // src/text.ts
552
+ var segmenter = new Intl.Segmenter();
553
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
554
+ function displayWidth(s) {
555
+ let w = 0;
556
+ for (const { segment } of segmenter.segment(s)) {
557
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
558
+ }
559
+ return w;
560
+ }
561
+ function padVisible(s, width) {
562
+ const w = displayWidth(s);
563
+ return w >= width ? s : s + " ".repeat(width - w);
564
+ }
565
+
505
566
  // src/normalize.ts
506
567
  function outcomeFromScore(home, away) {
507
568
  if (home > away) return "H";
@@ -3025,6 +3086,7 @@ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3025
3086
  var DEFAULT_COMPETITION = "fifa.world";
3026
3087
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3027
3088
  var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
3089
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3028
3090
  function competitionBase(slug) {
3029
3091
  return `${ESPN_SOCCER}/${slug}`;
3030
3092
  }
@@ -3068,9 +3130,15 @@ function toInt(s) {
3068
3130
  return Number.isFinite(n) ? n : void 0;
3069
3131
  }
3070
3132
  function toTeam(t2) {
3071
- const name = t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD";
3072
- const code = (t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3073
- return { code, name, flag: nationToFlag(t2?.displayName ?? t2?.abbreviation ?? name) };
3133
+ const name = sanitizeFeedText(
3134
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3135
+ );
3136
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3137
+ return {
3138
+ code,
3139
+ name,
3140
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3141
+ };
3074
3142
  }
3075
3143
  function mapEspnEvent(ev, ctx = {}) {
3076
3144
  const comp = ev.competitions?.[0];
@@ -3101,9 +3169,9 @@ function mapEspnEvent(ev, ctx = {}) {
3101
3169
  stage,
3102
3170
  group,
3103
3171
  kickoff: ev.date,
3104
- venue: comp?.venue?.fullName ?? "",
3105
- city: comp?.venue?.address?.city || void 0,
3106
- country: comp?.venue?.address?.country || void 0,
3172
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3173
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3174
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3107
3175
  home,
3108
3176
  away,
3109
3177
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3227,11 +3295,18 @@ var EspnAdapter = class {
3227
3295
  try {
3228
3296
  const res = await doFetch(url, {
3229
3297
  signal: controller.signal,
3298
+ // ESPN never legitimately redirects; following one would sidestep the
3299
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3300
+ redirect: "error",
3230
3301
  headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3231
3302
  });
3232
3303
  if (!res.ok) {
3233
3304
  throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3234
3305
  }
3306
+ const length = Number(res.headers?.get?.("content-length"));
3307
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3308
+ throw new Error(`ESPN response too large: ${length} bytes`);
3309
+ }
3235
3310
  return await res.json();
3236
3311
  } finally {
3237
3312
  clearTimeout(timer);
@@ -4567,12 +4642,19 @@ var PolymarketProvider = class {
4567
4642
  const doFetch = this.opts.fetchImpl ?? fetch;
4568
4643
  const res = await doFetch(url, {
4569
4644
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4645
+ // Gamma never legitimately redirects; following one would sidestep the
4646
+ // host allow-list (it only validates the base URL), so reject redirects.
4647
+ redirect: "error",
4570
4648
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4571
4649
  });
4572
4650
  if (res.status === 404) return void 0;
4573
4651
  if (!res.ok) {
4574
4652
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4575
4653
  }
4654
+ const length = Number(res.headers?.get?.("content-length"));
4655
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4656
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4657
+ }
4576
4658
  const data = await res.json();
4577
4659
  const event = Array.isArray(data) ? data[0] : data;
4578
4660
  return event && typeof event === "object" ? event : void 0;
@@ -5025,6 +5107,7 @@ export {
5025
5107
  DEFAULT_FLAVOR,
5026
5108
  DEFAULT_MAX_AGE_MS,
5027
5109
  EspnAdapter,
5110
+ FEED_TEXT_MAX,
5028
5111
  FLAVOR_LEVELS,
5029
5112
  FakeMarketProvider,
5030
5113
  LIVE_WINDOW_MS,
@@ -5043,6 +5126,7 @@ export {
5043
5126
  countdown,
5044
5127
  currentOrNextFixtureForTeam,
5045
5128
  deriveFavorite,
5129
+ displayWidth,
5046
5130
  favoriteStrength,
5047
5131
  fixturesByDate,
5048
5132
  fixturesByGroup,
@@ -5105,12 +5189,15 @@ export {
5105
5189
  normalizeLang,
5106
5190
  normalizeOutcomes,
5107
5191
  outcomeFromScore,
5192
+ padVisible,
5108
5193
  parseStandings,
5109
5194
  parseTeamSlot,
5110
5195
  resolveCompetition,
5111
5196
  resolveMarketSource,
5112
5197
  resolveTz,
5113
5198
  sanitizeBundledFixture,
5199
+ sanitizeFeedText,
5200
+ sanitizeMatchStrings,
5114
5201
  scoreline,
5115
5202
  stageLabel,
5116
5203
  stageLabelI18n,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
4
4
  "description": "Domain model, provider adapters (ESPN), standings, bundled schedule, and the read-only Polymarket market-signal sidecar powering Claudinho. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",