@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 +49 -1
- package/dist/index.js +93 -6
- package/package.json +1 -1
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 =
|
|
3072
|
-
|
|
3073
|
-
|
|
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.
|
|
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",
|