sbb-mcp 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +50 -57
- package/README.md +25 -214
- package/dist/index.js +47 -19
- package/package.json +10 -33
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +0 -44
- package/dist/auth.js.map +0 -1
- package/dist/cache.d.ts +0 -14
- package/dist/cache.js +0 -62
- package/dist/cache.js.map +0 -1
- package/dist/client.d.ts +0 -17
- package/dist/client.js +0 -70
- package/dist/client.js.map +0 -1
- package/dist/formatters.d.ts +0 -35
- package/dist/formatters.js +0 -285
- package/dist/formatters.js.map +0 -1
- package/dist/http.d.ts +0 -2
- package/dist/http.js +0 -117
- package/dist/http.js.map +0 -1
- package/dist/i18n.d.ts +0 -22
- package/dist/i18n.js +0 -36
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/journey.d.ts +0 -5
- package/dist/journey.js +0 -67
- package/dist/journey.js.map +0 -1
- package/dist/look2book.d.ts +0 -98
- package/dist/look2book.js +0 -212
- package/dist/look2book.js.map +0 -1
- package/dist/prices.d.ts +0 -3
- package/dist/prices.js +0 -51
- package/dist/prices.js.map +0 -1
- package/dist/profile.d.ts +0 -16
- package/dist/profile.js +0 -84
- package/dist/profile.js.map +0 -1
- package/dist/rate-limit.d.ts +0 -5
- package/dist/rate-limit.js +0 -44
- package/dist/rate-limit.js.map +0 -1
- package/dist/shortlink.d.ts +0 -60
- package/dist/shortlink.js +0 -122
- package/dist/shortlink.js.map +0 -1
- package/dist/structured.d.ts +0 -125
- package/dist/structured.js +0 -134
- package/dist/structured.js.map +0 -1
- package/dist/swisstrip.d.ts +0 -41
- package/dist/swisstrip.js +0 -135
- package/dist/swisstrip.js.map +0 -1
- package/dist/tools.d.ts +0 -40
- package/dist/tools.js +0 -509
- package/dist/tools.js.map +0 -1
- package/dist/transport/index.d.ts +0 -10
- package/dist/transport/index.js +0 -13
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/setup.d.ts +0 -1
- package/dist/transport/setup.js +0 -59
- package/dist/transport/setup.js.map +0 -1
- package/dist/transport/smapi-auth.d.ts +0 -14
- package/dist/transport/smapi-auth.js +0 -89
- package/dist/transport/smapi-auth.js.map +0 -1
- package/dist/transport/smapi-client.d.ts +0 -46
- package/dist/transport/smapi-client.js +0 -186
- package/dist/transport/smapi-client.js.map +0 -1
- package/dist/transport/smapi-journey.d.ts +0 -29
- package/dist/transport/smapi-journey.js +0 -91
- package/dist/transport/smapi-journey.js.map +0 -1
- package/dist/transport/smapi-mock.d.ts +0 -9
- package/dist/transport/smapi-mock.js +0 -151
- package/dist/transport/smapi-mock.js.map +0 -1
- package/dist/transport/smapi-prices.d.ts +0 -48
- package/dist/transport/smapi-prices.js +0 -144
- package/dist/transport/smapi-prices.js.map +0 -1
- package/dist/transport/smapi-types.d.ts +0 -181
- package/dist/transport/smapi-types.js +0 -2
- package/dist/transport/smapi-types.js.map +0 -1
- package/dist/types.d.ts +0 -139
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/widgets.d.ts +0 -60
- package/dist/widgets.js +0 -184
- package/dist/widgets.js.map +0 -1
- package/web/dist/widgets.css +0 -1
- package/web/dist/widgets.js +0 -1
package/dist/i18n.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Language helpers for sbb-mcp.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports the shared `sbb-i18n` primitives and adds MCP-specific helpers:
|
|
5
|
-
*
|
|
6
|
-
* - `toLocale(lang)` — maps our 9 language codes to BCP-47 locale tags for
|
|
7
|
-
* `Intl.DateTimeFormat`. Swiss languages get `-CH` (so e.g. dates render as
|
|
8
|
-
* DD.MM.YYYY for de-CH, not DD/MM/YYYY); non-Swiss use their primary region.
|
|
9
|
-
* - `SBB_LINK_LANGS` — languages SBB.ch actually supports in URL paths. The
|
|
10
|
-
* rest fall back to English for deep links.
|
|
11
|
-
*/
|
|
12
|
-
import type { Lang } from 'sbb-i18n';
|
|
13
|
-
export type { Lang, Translations } from 'sbb-i18n';
|
|
14
|
-
export { resolveLang, t, SUPPORTED_LANGS, isLang } from 'sbb-i18n';
|
|
15
|
-
/** BCP-47 locale tag for `Intl.DateTimeFormat`. Swiss languages use -CH variants. */
|
|
16
|
-
export declare function toLocale(lang: Lang): string;
|
|
17
|
-
/**
|
|
18
|
-
* Languages SBB.ch supports in its public URLs (/de, /fr, /it, /en).
|
|
19
|
-
* Other input languages fall back to English when building deep links.
|
|
20
|
-
*/
|
|
21
|
-
export declare const SBB_LINK_LANGS: readonly Lang[];
|
|
22
|
-
export declare function toSbbLinkLang(lang: Lang): 'de' | 'fr' | 'it' | 'en';
|
package/dist/i18n.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Language helpers for sbb-mcp.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports the shared `sbb-i18n` primitives and adds MCP-specific helpers:
|
|
5
|
-
*
|
|
6
|
-
* - `toLocale(lang)` — maps our 9 language codes to BCP-47 locale tags for
|
|
7
|
-
* `Intl.DateTimeFormat`. Swiss languages get `-CH` (so e.g. dates render as
|
|
8
|
-
* DD.MM.YYYY for de-CH, not DD/MM/YYYY); non-Swiss use their primary region.
|
|
9
|
-
* - `SBB_LINK_LANGS` — languages SBB.ch actually supports in URL paths. The
|
|
10
|
-
* rest fall back to English for deep links.
|
|
11
|
-
*/
|
|
12
|
-
export { resolveLang, t, SUPPORTED_LANGS, isLang } from 'sbb-i18n';
|
|
13
|
-
const LOCALE_MAP = {
|
|
14
|
-
de: 'de-CH',
|
|
15
|
-
fr: 'fr-CH',
|
|
16
|
-
it: 'it-CH',
|
|
17
|
-
en: 'en-CH',
|
|
18
|
-
es: 'es-ES',
|
|
19
|
-
pt: 'pt-PT',
|
|
20
|
-
ru: 'ru-RU',
|
|
21
|
-
ar: 'ar',
|
|
22
|
-
zh: 'zh-CN',
|
|
23
|
-
};
|
|
24
|
-
/** BCP-47 locale tag for `Intl.DateTimeFormat`. Swiss languages use -CH variants. */
|
|
25
|
-
export function toLocale(lang) {
|
|
26
|
-
return LOCALE_MAP[lang];
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Languages SBB.ch supports in its public URLs (/de, /fr, /it, /en).
|
|
30
|
-
* Other input languages fall back to English when building deep links.
|
|
31
|
-
*/
|
|
32
|
-
export const SBB_LINK_LANGS = ['de', 'fr', 'it', 'en'];
|
|
33
|
-
export function toSbbLinkLang(lang) {
|
|
34
|
-
return SBB_LINK_LANGS.includes(lang) ? lang : 'en';
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAElE,MAAM,UAAU,GAAyB;IACvC,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,IAAI;IACR,EAAE,EAAE,OAAO;CACZ,CAAA;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,IAAU;IACjC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAEhF,MAAM,UAAU,aAAa,CAAC,IAAU;IACtC,OAAQ,cAAoC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAkC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1G,CAAC"}
|
package/dist/index.d.ts
DELETED
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAA;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,KAAK,UAAU,IAAI;IACjB,2BAA2B,EAAE,CAAA;IAC7B,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAA;IAC5C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAE/B,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAA;QACjF,OAAO,CAAC,KAAK,CAAC,+EAA+E,CAAC,CAAA;IAChG,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,qBAAqB,EAAE,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAA;IACnF,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAA;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
package/dist/journey.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { Place, Trip, TripsCollection, TripSearchParams, PlaceSearchParams } from './types.js';
|
|
2
|
-
export declare function searchPlaces(params: PlaceSearchParams): Promise<Place[]>;
|
|
3
|
-
export declare function searchTrips(params: TripSearchParams): Promise<TripsCollection>;
|
|
4
|
-
export declare function getTrip(tripId: string, stopBehavior?: 'ORIGIN_DESTINATION_ONLY' | 'REAL_BOARDING_ALIGHTING'): Promise<Trip>;
|
|
5
|
-
export declare function paginateTrips(collectionId: string, direction: 'next' | 'previous'): Promise<TripsCollection>;
|
package/dist/journey.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { smapiRequest, getJourneyBaseUrl } from './client.js';
|
|
2
|
-
export async function searchPlaces(params) {
|
|
3
|
-
const body = {
|
|
4
|
-
placeInput: { name: params.name },
|
|
5
|
-
restrictions: {
|
|
6
|
-
...(params.type && { type: params.type }),
|
|
7
|
-
numberOfResults: params.numberOfResults ?? 10,
|
|
8
|
-
},
|
|
9
|
-
};
|
|
10
|
-
const result = await smapiRequest(getJourneyBaseUrl(), '/v1/places', { method: 'POST', body });
|
|
11
|
-
return result.places ?? [];
|
|
12
|
-
}
|
|
13
|
-
export async function searchTrips(params) {
|
|
14
|
-
const body = {
|
|
15
|
-
origin: {
|
|
16
|
-
objectType: 'StopPlaceRef',
|
|
17
|
-
stopPlaceRef: params.origin,
|
|
18
|
-
},
|
|
19
|
-
destination: {
|
|
20
|
-
objectType: 'StopPlaceRef',
|
|
21
|
-
stopPlaceRef: params.destination,
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
if (params.departureTime) {
|
|
25
|
-
body.departureTime = params.departureTime;
|
|
26
|
-
}
|
|
27
|
-
else if (params.arrivalTime) {
|
|
28
|
-
body.arrivalTime = params.arrivalTime;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
body.departureTime = new Date().toISOString();
|
|
32
|
-
}
|
|
33
|
-
if (params.transferLimit !== undefined) {
|
|
34
|
-
body.parameters = { transferLimit: params.transferLimit };
|
|
35
|
-
}
|
|
36
|
-
if (params.vias?.length) {
|
|
37
|
-
body.vias = params.vias.map((via) => ({
|
|
38
|
-
viaPlace: {
|
|
39
|
-
objectType: 'StopPlaceRef',
|
|
40
|
-
stopPlaceRef: via.stopPlaceRef,
|
|
41
|
-
},
|
|
42
|
-
...(via.dwellTime && { dwellTime: via.dwellTime }),
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
if (params.ptModeFilter) {
|
|
46
|
-
const parameters = body.parameters || {};
|
|
47
|
-
parameters.dataFilter = {
|
|
48
|
-
ptModeFilter: {
|
|
49
|
-
exclude: params.ptModeFilter.exclude,
|
|
50
|
-
transportModes: params.ptModeFilter.transportModes.map((mode) => ({
|
|
51
|
-
ptMode: mode,
|
|
52
|
-
})),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
body.parameters = parameters;
|
|
56
|
-
}
|
|
57
|
-
return smapiRequest(getJourneyBaseUrl(), '/v1/trips-collection', { method: 'POST', body });
|
|
58
|
-
}
|
|
59
|
-
export async function getTrip(tripId, stopBehavior = 'ORIGIN_DESTINATION_ONLY') {
|
|
60
|
-
const params = new URLSearchParams({ stopBehavior });
|
|
61
|
-
return smapiRequest(getJourneyBaseUrl(), `/v1/trips/${encodeURIComponent(tripId)}?${params}`);
|
|
62
|
-
}
|
|
63
|
-
export async function paginateTrips(collectionId, direction) {
|
|
64
|
-
const params = new URLSearchParams({ page: direction });
|
|
65
|
-
return smapiRequest(getJourneyBaseUrl(), `/v1/trips-collections/${encodeURIComponent(collectionId)}?${params}`);
|
|
66
|
-
}
|
|
67
|
-
//# sourceMappingURL=journey.js.map
|
package/dist/journey.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"journey.js","sourceRoot":"","sources":["../src/journey.ts"],"names":[],"mappings":"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"}
|
package/dist/look2book.d.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* look2book.ts — Look2Book ratio telemetry for SBB SMAPI calls.
|
|
3
|
-
*
|
|
4
|
-
* ## Why
|
|
5
|
-
* SBB's Distributionsvertrag caps the API-call-to-booking-click ratio:
|
|
6
|
-
* - Trips API (places, trips, trip-details, pagination): max 100 : 1
|
|
7
|
-
* - Offers API (prices, trip-offers): max 50 : 1
|
|
8
|
-
*
|
|
9
|
-
* Violating the ratio is a contractual issue, not a technical one — SBB
|
|
10
|
-
* audits quarterly. We need to: (1) observe the ratio in production so we
|
|
11
|
-
* can intervene before an audit finds us over, and (2) emit an operator
|
|
12
|
-
* warning when a running window crosses the threshold so the on-call can
|
|
13
|
-
* investigate (bot traffic? broken booking flow? missing CTAs?).
|
|
14
|
-
*
|
|
15
|
-
* ## What this module does — and doesn't do
|
|
16
|
-
* Does:
|
|
17
|
-
* - Count every SMAPI request (classified `trips` vs `offers` by base URL)
|
|
18
|
-
* and every booking-link click (from `/r/:token`) on a sliding 1-hour
|
|
19
|
-
* window.
|
|
20
|
-
* - Emit a JSON line per event to stdout so Railway's log drain (or any
|
|
21
|
-
* log forwarder) can ship it to long-term storage for trend analysis.
|
|
22
|
-
* - Log a `sbb_look2book_violation` warning JSON line once per 5-minute
|
|
23
|
-
* cooldown per family when the current window's ratio exceeds the cap.
|
|
24
|
-
* - Expose a synchronous `getRatioSnapshot()` so `/health` can surface the
|
|
25
|
-
* current state for readiness probes / dashboards.
|
|
26
|
-
*
|
|
27
|
-
* Does NOT:
|
|
28
|
-
* - Block or rate-limit violating requests. Look2Book is a business metric,
|
|
29
|
-
* not a request gate — gating tool calls based on a ratio would break UX
|
|
30
|
-
* catastrophically (e.g. a single user who hasn't clicked yet would see
|
|
31
|
-
* subsequent searches fail). See rate-limit.ts for the actual per-IP gate.
|
|
32
|
-
* - Persist counters. Railway restarts reset the window — acceptable since
|
|
33
|
-
* the contractual window is aggregate and real analysis happens offline
|
|
34
|
-
* via the JSON-line logs.
|
|
35
|
-
*
|
|
36
|
-
* ## Design: in-memory sliding window
|
|
37
|
-
* A single array of `{ts, kind}` events, pruned at every read/write so old
|
|
38
|
-
* entries fall off after `WINDOW_MS`. O(n) prune on each call, but n is
|
|
39
|
-
* bounded by the per-IP rate limit (60/min × 60min ≈ 3.6k events worst
|
|
40
|
-
* case) so this stays well under a millisecond.
|
|
41
|
-
*
|
|
42
|
-
* Events are timestamped with an injectable `now()` so tests can drive the
|
|
43
|
-
* window deterministically without `vi.useFakeTimers`.
|
|
44
|
-
*/
|
|
45
|
-
/** Tool family — matches the two SMAPI base URLs in smapi-client.ts. */
|
|
46
|
-
export type ApiFamily = 'trips' | 'offers';
|
|
47
|
-
/** Snapshot returned by getRatioSnapshot() — safe to JSON-serialize. */
|
|
48
|
-
export interface RatioSnapshot {
|
|
49
|
-
windowMs: number;
|
|
50
|
-
trips: {
|
|
51
|
-
calls: number;
|
|
52
|
-
clicks: number;
|
|
53
|
-
ratio: number | null;
|
|
54
|
-
cap: number;
|
|
55
|
-
violated: boolean;
|
|
56
|
-
};
|
|
57
|
-
offers: {
|
|
58
|
-
calls: number;
|
|
59
|
-
clicks: number;
|
|
60
|
-
ratio: number | null;
|
|
61
|
-
cap: number;
|
|
62
|
-
violated: boolean;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Record a successful SMAPI call. Call AFTER the request succeeds so failed
|
|
67
|
-
* requests (auth errors, network blips) don't pollute the ratio — only
|
|
68
|
-
* billable, data-returning calls count against the cap.
|
|
69
|
-
*
|
|
70
|
-
* The `endpoint` arg is logged for offline attribution (which tool is the
|
|
71
|
-
* worst offender?) but doesn't affect bucketing.
|
|
72
|
-
*/
|
|
73
|
-
export declare function recordApiCall(family: ApiFamily, endpoint: string): void;
|
|
74
|
-
/**
|
|
75
|
-
* Record a booking-link click (from /r/:token). The click "pays back" one
|
|
76
|
-
* unit of ratio for either family — SBB's contract counts all clicks toward
|
|
77
|
-
* both caps since a single click represents real user booking intent.
|
|
78
|
-
*/
|
|
79
|
-
export declare function recordClick(meta?: {
|
|
80
|
-
tripId?: string;
|
|
81
|
-
}): void;
|
|
82
|
-
/**
|
|
83
|
-
* Current window state. Cheap (bounded prune + single pass). Safe to call
|
|
84
|
-
* from a health endpoint handler without a cache.
|
|
85
|
-
*/
|
|
86
|
-
export declare function getRatioSnapshot(): RatioSnapshot;
|
|
87
|
-
/** Reset all state. Call between tests. */
|
|
88
|
-
export declare function __resetForTesting(): void;
|
|
89
|
-
/** Override the clock for deterministic window tests. */
|
|
90
|
-
export declare function __setClockForTesting(fn: () => number): void;
|
|
91
|
-
/** Expose tunables so tests can read them instead of duplicating the numbers. */
|
|
92
|
-
export declare const __internals: {
|
|
93
|
-
readonly WINDOW_MS: number;
|
|
94
|
-
readonly CAP_TRIPS: 100;
|
|
95
|
-
readonly CAP_OFFERS: 50;
|
|
96
|
-
readonly ALERT_COOLDOWN_MS: number;
|
|
97
|
-
readonly MIN_CALLS_BEFORE_ALERT: 10;
|
|
98
|
-
};
|
package/dist/look2book.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* look2book.ts — Look2Book ratio telemetry for SBB SMAPI calls.
|
|
3
|
-
*
|
|
4
|
-
* ## Why
|
|
5
|
-
* SBB's Distributionsvertrag caps the API-call-to-booking-click ratio:
|
|
6
|
-
* - Trips API (places, trips, trip-details, pagination): max 100 : 1
|
|
7
|
-
* - Offers API (prices, trip-offers): max 50 : 1
|
|
8
|
-
*
|
|
9
|
-
* Violating the ratio is a contractual issue, not a technical one — SBB
|
|
10
|
-
* audits quarterly. We need to: (1) observe the ratio in production so we
|
|
11
|
-
* can intervene before an audit finds us over, and (2) emit an operator
|
|
12
|
-
* warning when a running window crosses the threshold so the on-call can
|
|
13
|
-
* investigate (bot traffic? broken booking flow? missing CTAs?).
|
|
14
|
-
*
|
|
15
|
-
* ## What this module does — and doesn't do
|
|
16
|
-
* Does:
|
|
17
|
-
* - Count every SMAPI request (classified `trips` vs `offers` by base URL)
|
|
18
|
-
* and every booking-link click (from `/r/:token`) on a sliding 1-hour
|
|
19
|
-
* window.
|
|
20
|
-
* - Emit a JSON line per event to stdout so Railway's log drain (or any
|
|
21
|
-
* log forwarder) can ship it to long-term storage for trend analysis.
|
|
22
|
-
* - Log a `sbb_look2book_violation` warning JSON line once per 5-minute
|
|
23
|
-
* cooldown per family when the current window's ratio exceeds the cap.
|
|
24
|
-
* - Expose a synchronous `getRatioSnapshot()` so `/health` can surface the
|
|
25
|
-
* current state for readiness probes / dashboards.
|
|
26
|
-
*
|
|
27
|
-
* Does NOT:
|
|
28
|
-
* - Block or rate-limit violating requests. Look2Book is a business metric,
|
|
29
|
-
* not a request gate — gating tool calls based on a ratio would break UX
|
|
30
|
-
* catastrophically (e.g. a single user who hasn't clicked yet would see
|
|
31
|
-
* subsequent searches fail). See rate-limit.ts for the actual per-IP gate.
|
|
32
|
-
* - Persist counters. Railway restarts reset the window — acceptable since
|
|
33
|
-
* the contractual window is aggregate and real analysis happens offline
|
|
34
|
-
* via the JSON-line logs.
|
|
35
|
-
*
|
|
36
|
-
* ## Design: in-memory sliding window
|
|
37
|
-
* A single array of `{ts, kind}` events, pruned at every read/write so old
|
|
38
|
-
* entries fall off after `WINDOW_MS`. O(n) prune on each call, but n is
|
|
39
|
-
* bounded by the per-IP rate limit (60/min × 60min ≈ 3.6k events worst
|
|
40
|
-
* case) so this stays well under a millisecond.
|
|
41
|
-
*
|
|
42
|
-
* Events are timestamped with an injectable `now()` so tests can drive the
|
|
43
|
-
* window deterministically without `vi.useFakeTimers`.
|
|
44
|
-
*/
|
|
45
|
-
// ─── Tunables ───────────────────────────────────────────────────────────────
|
|
46
|
-
/** Rolling window length. 1 hour balances responsiveness vs noise. */
|
|
47
|
-
const WINDOW_MS = 60 * 60 * 1000;
|
|
48
|
-
/** Maximum API-calls-to-clicks ratio per family — contractual caps. */
|
|
49
|
-
const CAP_TRIPS = 100;
|
|
50
|
-
const CAP_OFFERS = 50;
|
|
51
|
-
/** Minimum gap between duplicate violation warnings per family. */
|
|
52
|
-
const ALERT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
53
|
-
/**
|
|
54
|
-
* Don't warn on the first few calls of a fresh window — a single call with
|
|
55
|
-
* zero clicks gives infinite ratio, which is technically a violation but
|
|
56
|
-
* operationally meaningless. Wait until we have enough calls to reason about.
|
|
57
|
-
*/
|
|
58
|
-
const MIN_CALLS_BEFORE_ALERT = 10;
|
|
59
|
-
// ─── Mutable module state ───────────────────────────────────────────────────
|
|
60
|
-
const events = [];
|
|
61
|
-
const lastAlertTs = { trips: 0, offers: 0 };
|
|
62
|
-
/** Clock injector for deterministic tests. Defaults to Date.now. */
|
|
63
|
-
let nowFn = () => Date.now();
|
|
64
|
-
// ─── Internals ──────────────────────────────────────────────────────────────
|
|
65
|
-
function pruneOld(now) {
|
|
66
|
-
const cutoff = now - WINDOW_MS;
|
|
67
|
-
// Array.shift is O(n) but events is bounded (~3.6k max); fine.
|
|
68
|
-
while (events.length > 0 && events[0].ts < cutoff) {
|
|
69
|
-
events.shift();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function countsFor(family) {
|
|
73
|
-
let calls = 0;
|
|
74
|
-
let clicks = 0;
|
|
75
|
-
for (const ev of events) {
|
|
76
|
-
if (ev.kind === family)
|
|
77
|
-
calls++;
|
|
78
|
-
else if (ev.kind === 'click')
|
|
79
|
-
clicks++;
|
|
80
|
-
}
|
|
81
|
-
return { calls, clicks };
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Current ratio for a family. Returns `null` when no calls yet (undefined
|
|
85
|
-
* ratio). When calls > 0 and clicks == 0, returns `Infinity` — callers
|
|
86
|
-
* should treat this as "over the cap" but violation logic also checks
|
|
87
|
-
* MIN_CALLS_BEFORE_ALERT to avoid spurious warnings.
|
|
88
|
-
*/
|
|
89
|
-
function ratioFor(family) {
|
|
90
|
-
const { calls, clicks } = countsFor(family);
|
|
91
|
-
if (calls === 0)
|
|
92
|
-
return null;
|
|
93
|
-
if (clicks === 0)
|
|
94
|
-
return Infinity;
|
|
95
|
-
return calls / clicks;
|
|
96
|
-
}
|
|
97
|
-
function capFor(family) {
|
|
98
|
-
return family === 'trips' ? CAP_TRIPS : CAP_OFFERS;
|
|
99
|
-
}
|
|
100
|
-
function logJson(line) {
|
|
101
|
-
// Single JSON line per event — consistent with the /r/:token click log.
|
|
102
|
-
// Using stdout directly; Railway (and most log drains) capture it verbatim.
|
|
103
|
-
console.log(JSON.stringify(line));
|
|
104
|
-
}
|
|
105
|
-
function checkAndWarn(family, now) {
|
|
106
|
-
const { calls, clicks } = countsFor(family);
|
|
107
|
-
if (calls < MIN_CALLS_BEFORE_ALERT)
|
|
108
|
-
return;
|
|
109
|
-
const cap = capFor(family);
|
|
110
|
-
const ratio = ratioFor(family);
|
|
111
|
-
if (ratio === null || ratio <= cap)
|
|
112
|
-
return;
|
|
113
|
-
if (now - lastAlertTs[family] < ALERT_COOLDOWN_MS)
|
|
114
|
-
return;
|
|
115
|
-
lastAlertTs[family] = now;
|
|
116
|
-
console.warn(JSON.stringify({
|
|
117
|
-
event: 'sbb_look2book_violation',
|
|
118
|
-
ts: new Date(now).toISOString(),
|
|
119
|
-
family,
|
|
120
|
-
calls,
|
|
121
|
-
clicks,
|
|
122
|
-
ratio: ratio === Infinity ? 'infinity' : Number(ratio.toFixed(2)),
|
|
123
|
-
cap,
|
|
124
|
-
windowMs: WINDOW_MS,
|
|
125
|
-
}));
|
|
126
|
-
}
|
|
127
|
-
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
128
|
-
/**
|
|
129
|
-
* Record a successful SMAPI call. Call AFTER the request succeeds so failed
|
|
130
|
-
* requests (auth errors, network blips) don't pollute the ratio — only
|
|
131
|
-
* billable, data-returning calls count against the cap.
|
|
132
|
-
*
|
|
133
|
-
* The `endpoint` arg is logged for offline attribution (which tool is the
|
|
134
|
-
* worst offender?) but doesn't affect bucketing.
|
|
135
|
-
*/
|
|
136
|
-
export function recordApiCall(family, endpoint) {
|
|
137
|
-
const now = nowFn();
|
|
138
|
-
pruneOld(now);
|
|
139
|
-
events.push({ ts: now, kind: family, endpoint });
|
|
140
|
-
logJson({
|
|
141
|
-
event: 'sbb_smapi_call',
|
|
142
|
-
ts: new Date(now).toISOString(),
|
|
143
|
-
family,
|
|
144
|
-
endpoint,
|
|
145
|
-
});
|
|
146
|
-
checkAndWarn(family, now);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Record a booking-link click (from /r/:token). The click "pays back" one
|
|
150
|
-
* unit of ratio for either family — SBB's contract counts all clicks toward
|
|
151
|
-
* both caps since a single click represents real user booking intent.
|
|
152
|
-
*/
|
|
153
|
-
export function recordClick(meta = {}) {
|
|
154
|
-
const now = nowFn();
|
|
155
|
-
pruneOld(now);
|
|
156
|
-
events.push({ ts: now, kind: 'click' });
|
|
157
|
-
logJson({
|
|
158
|
-
event: 'sbb_look2book_click',
|
|
159
|
-
ts: new Date(now).toISOString(),
|
|
160
|
-
...(meta.tripId && { tripId: meta.tripId }),
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Current window state. Cheap (bounded prune + single pass). Safe to call
|
|
165
|
-
* from a health endpoint handler without a cache.
|
|
166
|
-
*/
|
|
167
|
-
export function getRatioSnapshot() {
|
|
168
|
-
const now = nowFn();
|
|
169
|
-
pruneOld(now);
|
|
170
|
-
const t = countsFor('trips');
|
|
171
|
-
const o = countsFor('offers');
|
|
172
|
-
const tRatio = ratioFor('trips');
|
|
173
|
-
const oRatio = ratioFor('offers');
|
|
174
|
-
return {
|
|
175
|
-
windowMs: WINDOW_MS,
|
|
176
|
-
trips: {
|
|
177
|
-
calls: t.calls,
|
|
178
|
-
clicks: t.clicks,
|
|
179
|
-
ratio: tRatio === Infinity ? null : tRatio,
|
|
180
|
-
cap: CAP_TRIPS,
|
|
181
|
-
violated: tRatio !== null && tRatio > CAP_TRIPS,
|
|
182
|
-
},
|
|
183
|
-
offers: {
|
|
184
|
-
calls: o.calls,
|
|
185
|
-
clicks: o.clicks,
|
|
186
|
-
ratio: oRatio === Infinity ? null : oRatio,
|
|
187
|
-
cap: CAP_OFFERS,
|
|
188
|
-
violated: oRatio !== null && oRatio > CAP_OFFERS,
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
// ─── Test hooks ─────────────────────────────────────────────────────────────
|
|
193
|
-
/** Reset all state. Call between tests. */
|
|
194
|
-
export function __resetForTesting() {
|
|
195
|
-
events.length = 0;
|
|
196
|
-
lastAlertTs.trips = 0;
|
|
197
|
-
lastAlertTs.offers = 0;
|
|
198
|
-
nowFn = () => Date.now();
|
|
199
|
-
}
|
|
200
|
-
/** Override the clock for deterministic window tests. */
|
|
201
|
-
export function __setClockForTesting(fn) {
|
|
202
|
-
nowFn = fn;
|
|
203
|
-
}
|
|
204
|
-
/** Expose tunables so tests can read them instead of duplicating the numbers. */
|
|
205
|
-
export const __internals = {
|
|
206
|
-
WINDOW_MS,
|
|
207
|
-
CAP_TRIPS,
|
|
208
|
-
CAP_OFFERS,
|
|
209
|
-
ALERT_COOLDOWN_MS,
|
|
210
|
-
MIN_CALLS_BEFORE_ALERT,
|
|
211
|
-
};
|
|
212
|
-
//# sourceMappingURL=look2book.js.map
|
package/dist/look2book.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"look2book.js","sourceRoot":"","sources":["../src/look2book.ts"],"names":[],"mappings":"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"}
|
package/dist/prices.d.ts
DELETED
package/dist/prices.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { smapiRequest, getTicketingBaseUrl } from './client.js';
|
|
2
|
-
export async function getTripPrices(tripIds, travelers) {
|
|
3
|
-
const results = await Promise.all(tripIds.map(async (tripId) => {
|
|
4
|
-
const params = new URLSearchParams({ tripId });
|
|
5
|
-
const path = travelers?.length
|
|
6
|
-
? `/api/v2/prices?${params}`
|
|
7
|
-
: `/api/prices?${params}`;
|
|
8
|
-
try {
|
|
9
|
-
const data = await smapiRequest(getTicketingBaseUrl(), path, {
|
|
10
|
-
method: travelers?.length ? 'POST' : 'GET',
|
|
11
|
-
...(travelers?.length && { body: { travelers } }),
|
|
12
|
-
});
|
|
13
|
-
const prices = data.prices ?? (data.price ? [{
|
|
14
|
-
amount: data.price.amount,
|
|
15
|
-
currency: data.price.currency,
|
|
16
|
-
class: '2',
|
|
17
|
-
}] : []);
|
|
18
|
-
return {
|
|
19
|
-
tripId,
|
|
20
|
-
prices: prices.map((p) => ({
|
|
21
|
-
amount: p.amount,
|
|
22
|
-
currency: p.currency || 'CHF',
|
|
23
|
-
class: (p.class || '2'),
|
|
24
|
-
reductionCard: p.reductionCard,
|
|
25
|
-
})),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
console.error(`[sbb-mcp] Failed to get price for trip ${tripId}:`, err);
|
|
30
|
-
return { tripId, prices: [] };
|
|
31
|
-
}
|
|
32
|
-
}));
|
|
33
|
-
return results;
|
|
34
|
-
}
|
|
35
|
-
export async function getTripOfferLink(tripId, travelers) {
|
|
36
|
-
const body = {
|
|
37
|
-
tripId,
|
|
38
|
-
travelers: travelers.map((t) => ({
|
|
39
|
-
externalId: t.id,
|
|
40
|
-
type: t.type,
|
|
41
|
-
...(t.dateOfBirth && { dateOfBirth: t.dateOfBirth }),
|
|
42
|
-
...(t.reductionCard && t.reductionCard !== 'NONE' && {
|
|
43
|
-
cards: [{ type: t.reductionCard }],
|
|
44
|
-
}),
|
|
45
|
-
})),
|
|
46
|
-
};
|
|
47
|
-
const data = await smapiRequest(getTicketingBaseUrl(), '/api/trip-offers', { method: 'POST', body });
|
|
48
|
-
const links = (data.links ?? []);
|
|
49
|
-
return links.find((l) => l.rel === 'online-offers')?.href ?? null;
|
|
50
|
-
}
|
|
51
|
-
//# sourceMappingURL=prices.js.map
|
package/dist/prices.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prices.js","sourceRoot":"","sources":["../src/prices.ts"],"names":[],"mappings":"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"}
|
package/dist/profile.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Lang } from './i18n.js';
|
|
2
|
-
export type UserProfile = {
|
|
3
|
-
first_name?: string;
|
|
4
|
-
last_name?: string;
|
|
5
|
-
date_of_birth?: string;
|
|
6
|
-
reduction_card?: 'HALF_FARE' | 'GA' | 'NONE';
|
|
7
|
-
reduction_card_valid_until?: string;
|
|
8
|
-
language?: Lang;
|
|
9
|
-
created_at?: string;
|
|
10
|
-
updated_at?: string;
|
|
11
|
-
};
|
|
12
|
-
export declare function getProfilePath(): string;
|
|
13
|
-
export declare function loadProfile(): UserProfile | null;
|
|
14
|
-
export declare function saveProfile(profile: UserProfile): void;
|
|
15
|
-
export declare function isReductionCardExpired(profile: UserProfile): boolean;
|
|
16
|
-
export declare function formatProfileSummary(profile: UserProfile, lang?: Lang): string;
|
package/dist/profile.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { toLocale, t } from './i18n.js';
|
|
5
|
-
// Path resolution is lazy (per call) rather than a module-level constant so
|
|
6
|
-
// tests can redirect storage via `SBB_MCP_PROFILE_DIR` without re-importing.
|
|
7
|
-
// Power users can also set the env var to sandbox the profile file under a
|
|
8
|
-
// custom directory (e.g. for multi-account setups).
|
|
9
|
-
function getProfileDir() {
|
|
10
|
-
return process.env.SBB_MCP_PROFILE_DIR || join(homedir(), '.sbb-mcp');
|
|
11
|
-
}
|
|
12
|
-
export function getProfilePath() {
|
|
13
|
-
return join(getProfileDir(), 'profile.json');
|
|
14
|
-
}
|
|
15
|
-
export function loadProfile() {
|
|
16
|
-
try {
|
|
17
|
-
const path = getProfilePath();
|
|
18
|
-
if (!existsSync(path))
|
|
19
|
-
return null;
|
|
20
|
-
const raw = readFileSync(path, 'utf-8');
|
|
21
|
-
return JSON.parse(raw);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export function saveProfile(profile) {
|
|
28
|
-
const dir = getProfileDir();
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
const existing = loadProfile();
|
|
31
|
-
const merged = {
|
|
32
|
-
...existing,
|
|
33
|
-
...profile,
|
|
34
|
-
updated_at: new Date().toISOString(),
|
|
35
|
-
created_at: existing?.created_at || new Date().toISOString(),
|
|
36
|
-
};
|
|
37
|
-
writeFileSync(getProfilePath(), JSON.stringify(merged, null, 2), 'utf-8');
|
|
38
|
-
}
|
|
39
|
-
export function isReductionCardExpired(profile) {
|
|
40
|
-
if (!profile.reduction_card || profile.reduction_card === 'NONE')
|
|
41
|
-
return false;
|
|
42
|
-
if (!profile.reduction_card_valid_until)
|
|
43
|
-
return false; // no expiry set, assume valid
|
|
44
|
-
const validUntil = new Date(profile.reduction_card_valid_until).getTime();
|
|
45
|
-
return Date.now() > validUntil;
|
|
46
|
-
}
|
|
47
|
-
export function formatProfileSummary(profile, lang = 'en') {
|
|
48
|
-
const tr = t(lang);
|
|
49
|
-
const locale = toLocale(lang);
|
|
50
|
-
const lines = ['**Your Travel Profile**', ''];
|
|
51
|
-
if (profile.first_name || profile.last_name) {
|
|
52
|
-
lines.push(`Name: ${[profile.first_name, profile.last_name].filter(Boolean).join(' ')}`);
|
|
53
|
-
}
|
|
54
|
-
if (profile.date_of_birth) {
|
|
55
|
-
lines.push(`Date of birth: ${profile.date_of_birth}`);
|
|
56
|
-
}
|
|
57
|
-
if (profile.reduction_card && profile.reduction_card !== 'NONE') {
|
|
58
|
-
const cardName = profile.reduction_card === 'HALF_FARE' ? 'Halbtax (Half-Fare)' : 'GA Travelcard';
|
|
59
|
-
const expired = isReductionCardExpired(profile);
|
|
60
|
-
let cardLine = `Reduction card: ${cardName}`;
|
|
61
|
-
if (profile.reduction_card_valid_until) {
|
|
62
|
-
cardLine += ` (valid until ${profile.reduction_card_valid_until})`;
|
|
63
|
-
}
|
|
64
|
-
if (expired) {
|
|
65
|
-
cardLine += ' — EXPIRED. Ask the user if they renewed it.';
|
|
66
|
-
}
|
|
67
|
-
lines.push(cardLine);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
lines.push('Reduction card: None');
|
|
71
|
-
}
|
|
72
|
-
if (profile.language) {
|
|
73
|
-
lines.push(`Language: ${profile.language}`);
|
|
74
|
-
}
|
|
75
|
-
if (profile.updated_at) {
|
|
76
|
-
lines.push(`Last updated: ${new Date(profile.updated_at).toLocaleDateString(locale)}`);
|
|
77
|
-
}
|
|
78
|
-
// Suppress unused-var warning for tr until we translate the labels above
|
|
79
|
-
void tr;
|
|
80
|
-
lines.push('');
|
|
81
|
-
lines.push(`*Profile stored at \`${getProfilePath()}\`. Edit or delete anytime.*`);
|
|
82
|
-
return lines.join('\n');
|
|
83
|
-
}
|
|
84
|
-
//# sourceMappingURL=profile.js.map
|
package/dist/profile.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"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"}
|