migros-mcp 0.3.1 → 0.4.0
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/README.md +28 -20
- package/dist/auth/api.js +18 -1
- package/dist/auth/api.js.map +1 -1
- package/dist/auth/cookies.d.ts +11 -0
- package/dist/auth/cookies.js +4 -0
- package/dist/auth/cookies.js.map +1 -1
- package/dist/auth/index.d.ts +20 -3
- package/dist/auth/index.js +75 -21
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/login.d.ts +20 -2
- package/dist/auth/login.js +48 -5
- package/dist/auth/login.js.map +1 -1
- package/dist/auth/oauth.d.ts +11 -1
- package/dist/auth/oauth.js +41 -13
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/rate-limit.d.ts +70 -0
- package/dist/auth/rate-limit.js +117 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth-tools/_shared.d.ts +1 -1
- package/dist/auth-tools/_shared.js +2 -2
- package/dist/auth-tools/_shared.js.map +1 -1
- package/dist/auth-tools/authenticate.d.ts +10 -0
- package/dist/auth-tools/authenticate.js +17 -0
- package/dist/auth-tools/authenticate.js.map +1 -0
- package/dist/auth-tools/cart.d.ts +19 -1
- package/dist/auth-tools/cart.js +38 -4
- package/dist/auth-tools/cart.js.map +1 -1
- package/dist/auth-tools.d.ts +2 -1
- package/dist/auth-tools.js +2 -1
- package/dist/auth-tools.js.map +1 -1
- package/dist/index.d.ts +0 -0
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/migros.d.ts +0 -0
- package/dist/migros.js +0 -0
- package/dist/migros.js.map +0 -0
- package/dist/schemas.d.ts +14 -0
- package/dist/schemas.js +15 -2
- package/dist/schemas.js.map +1 -1
- package/dist/tools.d.ts +0 -0
- package/dist/tools.js +0 -0
- package/dist/tools.js.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Search [Migros](https://www.migros.ch) products and (optionally) manage your bas
|
|
|
30
30
|
- "Which Cumulus coupons do I have right now?"
|
|
31
31
|
- "How many Cumulus points do I have?"
|
|
32
32
|
|
|
33
|
-
The basket and order tools require your Migros account credentials. They're optional
|
|
33
|
+
The basket and order tools require your Migros account credentials. They're optional. If you skip them at install, the anonymous tools still work.
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
@@ -39,9 +39,9 @@ The basket and order tools require your Migros account credentials. They're opti
|
|
|
39
39
|
1. Download [migros-mcp.mcpb](https://github.com/lewpgs/migros-mcp/releases/latest/download/migros-mcp.mcpb)
|
|
40
40
|
2. Install it:
|
|
41
41
|
|
|
42
|
-
**macOS
|
|
42
|
+
**macOS**: Double-click the file, or drag and drop it onto the Claude Desktop app icon
|
|
43
43
|
|
|
44
|
-
**Windows
|
|
44
|
+
**Windows**: In Claude Desktop, go to File > Settings > Extensions > Advanced Settings > Install Extension and select the file
|
|
45
45
|
|
|
46
46
|
3. Optional: enter your Migros email, password, and TOTP secret if you want the basket/order tools. Leave all three blank for anonymous access only. Credentials are stored in your OS keychain by Claude Desktop.
|
|
47
47
|
|
|
@@ -49,13 +49,15 @@ The basket and order tools require your Migros account credentials. They're opti
|
|
|
49
49
|
|
|
50
50
|
How to find it:
|
|
51
51
|
|
|
52
|
-
- **If you saved it during enrollment
|
|
53
|
-
- **If you didn't save it
|
|
52
|
+
- **If you saved it during enrollment**: copy it from your password manager. In 1Password / Bitwarden / Proton Pass, open the Migros entry, edit the OTP field, choose *"View one-time password secret"* (or similar). The string after `secret=` is what you want.
|
|
53
|
+
- **If you didn't save it**: Migros only shows the seed during initial 2FA setup, on the same screen as the QR code, in a box labeled *"Or enter the following key in the authenticator app:"*. Once enrollment completes, the seed is gone for good.
|
|
54
54
|
|
|
55
|
-
To recover it: in Migros account settings, **remove** your existing 2FA, then **set it up again**. The new setup screen will show the new seed text below the QR code
|
|
55
|
+
To recover it: in Migros account settings, **remove** your existing 2FA, then **set it up again**. The new setup screen will show the new seed text below the QR code. Copy it before completing enrollment, and re-add the new entry to your authenticator app.
|
|
56
56
|
|
|
57
57
|
Leave the field blank entirely if your Migros account doesn't have 2FA enabled.
|
|
58
58
|
|
|
59
|
+
**Rather not store the seed?** Leave the TOTP field blank. When Claude needs to log in, it asks you for the current 6-digit code from your authenticator app (or reads it from a password manager it has access to) and passes it to the `authenticate` tool. The code is used for that one login and is not stored.
|
|
60
|
+
|
|
59
61
|
That's it.
|
|
60
62
|
|
|
61
63
|
---
|
|
@@ -82,8 +84,8 @@ claude mcp add migros \
|
|
|
82
84
|
|
|
83
85
|
Add to your config:
|
|
84
86
|
|
|
85
|
-
- **Claude Desktop
|
|
86
|
-
- **Cursor
|
|
87
|
+
- **Claude Desktop**: `claude_desktop_config.json`
|
|
88
|
+
- **Cursor**: `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
|
|
87
89
|
|
|
88
90
|
```json
|
|
89
91
|
{
|
|
@@ -101,7 +103,7 @@ Add to your config:
|
|
|
101
103
|
}
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
The `env` block is optional. Omit it to use anonymous tools only.
|
|
106
|
+
The `env` block is optional. Omit it to use anonymous tools only. Leave out `MIGROS_TOTP_SECRET` if you'd rather give Claude the current 6-digit code when it needs to log in (see `authenticate` below).
|
|
105
107
|
|
|
106
108
|
### Test connection
|
|
107
109
|
|
|
@@ -112,7 +114,7 @@ MIGROS_EMAIL=... MIGROS_PASSWORD=... MIGROS_TOTP_SECRET=... npx migros-mcp # a
|
|
|
112
114
|
|
|
113
115
|
## Updating
|
|
114
116
|
|
|
115
|
-
The recommended install commands above use `npx -y migros-mcp` without pinning a version, so you auto-receive new releases.
|
|
117
|
+
The recommended install commands above use `npx -y migros-mcp` without pinning a version, so you auto-receive new releases. Recent npm versions check for the newest version each time `npx` starts, so an update lands the next time your MCP host starts the server. If it seems stuck on an old version, run `npm cache clean --force` and restart the host.
|
|
116
118
|
|
|
117
119
|
If you've installed via the **drag-installed `.mcpb`** instead, Claude Desktop pins you to that specific version. Download the latest `.mcpb` from [Releases](https://github.com/lewpgs/migros-mcp/releases) and re-install to update. Your credentials in the OS keychain are preserved across re-installs.
|
|
118
120
|
|
|
@@ -132,14 +134,16 @@ If you've **explicitly pinned** a version in your config (e.g. `migros-mcp@0.3.0
|
|
|
132
134
|
| `get_promotions` | Search current Migros promotions and deals. |
|
|
133
135
|
| `search_recipes` | Search Migusto (Migros' recipe site) by query and/or ingredients. German/French/Italian only. |
|
|
134
136
|
| `get_recipe_details` | Full recipe by slug: ingredients, instructions, photos, nutrition. |
|
|
135
|
-
| `get_recipe_products` | Migros product UIDs for a recipe
|
|
137
|
+
| `get_recipe_products` | Migros product UIDs for a recipe, ready to pass straight to `add_to_basket`. |
|
|
136
138
|
|
|
137
139
|
### Authenticated (require credentials)
|
|
138
140
|
|
|
139
141
|
| Tool | Description |
|
|
140
142
|
|---|---|
|
|
143
|
+
| `authenticate` | Log in with the current six-digit 2FA code. Only needed when no TOTP secret is set. Reuses a cached session when possible. |
|
|
141
144
|
| `get_profile` | Logged-in customer's basic profile (name, email, language). |
|
|
142
145
|
| `get_addresses` | Saved delivery and billing addresses. |
|
|
146
|
+
| `get_shopping_lists` | The user's shopping lists by name, for people with more than one. Pass a list's id to the basket tools. |
|
|
143
147
|
| `get_basket` | Items currently in the user's basket. |
|
|
144
148
|
| `add_to_basket` | Add a product (sets target quantity, default 1). |
|
|
145
149
|
| `update_basket_quantity` | Set an item's exact quantity (0 to remove). |
|
|
@@ -158,7 +162,9 @@ If you've **explicitly pinned** a version in your config (e.g. `migros-mcp@0.3.0
|
|
|
158
162
|
Use the [migros-api-wrapper](https://www.npmjs.com/package/migros-api-wrapper) which calls the same endpoints the Migros website uses internally. A guest token is fetched at startup and cached.
|
|
159
163
|
|
|
160
164
|
### Authenticated tools
|
|
161
|
-
Use OAuth 2.0 against `login.migros.ch`. The first call runs a credentialed login (email → password → TOTP) and caches the resulting cookies + access token. Every 30 minutes the token is refreshed silently using cached cookies
|
|
165
|
+
Use OAuth 2.0 against `login.migros.ch`. The first call runs a credentialed login (email → password → TOTP) and caches the resulting cookies + access token. Every 30 minutes the token is refreshed silently using cached cookies, without submitting the login form again.
|
|
166
|
+
|
|
167
|
+
With `MIGROS_TOTP_SECRET` set, the MCP makes the 2FA code itself. Without it, the AI passes the current code to `authenticate`: it asks the user, or reads it from a password manager it has access to (for example a 1Password MCP or the 1Password CLI). The code is used for that one login and is never stored or logged.
|
|
162
168
|
|
|
163
169
|
The cache is persisted across MCP process restarts (Claude Desktop launches, Mac reboots, etc.) so we re-authenticate maybe twice a year instead of dozens of times a day. Migros' login endpoint is behind Cloudflare bot detection that throttles frequent logins, so persistence is what keeps the MCP from getting rate-limited.
|
|
164
170
|
|
|
@@ -170,14 +176,16 @@ The session lives at:
|
|
|
170
176
|
|
|
171
177
|
If the cached session expires (typically a few weeks), the MCP automatically re-runs the credentialed login.
|
|
172
178
|
|
|
179
|
+
Without a stored TOTP secret, authenticated tools return `fresh_code_required` instead, and the AI calls `authenticate` with a new code. The MCP remembers that the account needs a code, so it doesn't send your password to Migros until it has one.
|
|
180
|
+
|
|
173
181
|
### Order placement
|
|
174
182
|
For safety, this MCP **does not** place orders programmatically. The `get_checkout_link` tool returns a URL the user opens in their real browser to confirm address, delivery slot, and payment via Migros' own checkout flow. Real money requires a real human click.
|
|
175
183
|
|
|
176
184
|
## Known limitations
|
|
177
185
|
|
|
178
|
-
- **2FA support is TOTP-only.** Accounts secured with a passkey (and no TOTP fallback) are not supported
|
|
179
|
-
- **No automatic order placement.** See above
|
|
180
|
-
- **Cloudflare rate limits.** If the MCP
|
|
186
|
+
- **2FA support is TOTP-only.** Accounts secured with a passkey (and no TOTP fallback) are not supported. If your Migros account uses passkey as the only second factor, add a TOTP authenticator app in Migros account settings, then provide the TOTP secret to this MCP, or pass the current code to `authenticate`.
|
|
187
|
+
- **No automatic order placement.** See above: `get_checkout_link` hands off to your browser for the actual placement.
|
|
188
|
+
- **Cloudflare rate limits.** The login host (`login.migros.ch`) sits behind Cloudflare bot detection. If the MCP triggers too many login attempts in quick succession (wrong password retries, repeated restarts without a cached session), the IP may be throttled. When this happens the MCP now sets a **cooldown** (respecting `Retry-After` headers or falling back to 30 s → 2 min → 10 min backoff) and **stops hitting the login host** until it clears. During cooldown, any **still-valid cached JWT keeps working** so your MCP tools may continue serving requests. Only re-authentication is paused. If the cached JWT is also expired while rate-limited the MCP will surface a clear error with the cooldown deadline rather than silently retrying and worsening the throttle.
|
|
181
189
|
|
|
182
190
|
## Development
|
|
183
191
|
|
|
@@ -197,11 +205,11 @@ npx -y @modelcontextprotocol/inspector npx migros-mcp
|
|
|
197
205
|
|
|
198
206
|
## Disclaimers
|
|
199
207
|
|
|
200
|
-
- **Unofficial
|
|
201
|
-
- **No official API
|
|
202
|
-
- **Credentials
|
|
203
|
-
- **No order placement
|
|
204
|
-
- **Use at your own risk
|
|
208
|
+
- **Unofficial**: Not affiliated with, endorsed by, or connected to Migros in any way.
|
|
209
|
+
- **No official API**: Uses endpoints that power the Migros website. They are not documented or supported. Migros can change them without notice.
|
|
210
|
+
- **Credentials**: When you provide credentials, they're used only to authenticate against `login.migros.ch`. A 6-digit code passed to `authenticate` is used for that one login and is never stored or logged. Resulting session cookies and access tokens are stored locally in your OS user config directory. They are never sent anywhere except Migros' own servers.
|
|
211
|
+
- **No order placement**: This server does not place orders programmatically. Orders are completed in the user's own browser via the URL returned by `get_checkout_link`.
|
|
212
|
+
- **Use at your own risk**: No guarantees of functionality, availability, or compatibility.
|
|
205
213
|
|
|
206
214
|
## License
|
|
207
215
|
|
package/dist/auth/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { client, silentOAuth, uniformHeaders } from "./oauth.js";
|
|
2
2
|
import { ingestSetCookie, loadSession, saveSession } from "./cookies.js";
|
|
3
3
|
import { getJwt } from "./index.js";
|
|
4
|
+
import { isRateLimited, RateLimitError } from "./rate-limit.js";
|
|
4
5
|
const BASE = "https://www.migros.ch";
|
|
5
6
|
async function makeRequest(method, path, body, jwt, language, session) {
|
|
6
7
|
const headers = uniformHeaders("www.migros.ch", session, {
|
|
@@ -41,8 +42,17 @@ export async function api(method, path, body, opts = {}) {
|
|
|
41
42
|
saveSession(session);
|
|
42
43
|
// Token rejected? Force a silent refresh and retry once.
|
|
43
44
|
if (r.status === 401) {
|
|
45
|
+
session = loadSession();
|
|
46
|
+
// Never hammer the rate-limited host: if a cooldown is active, skip the
|
|
47
|
+
// refresh entirely and surface a clear error instead of firing another
|
|
48
|
+
// login-host request (which would 429 and extend the throttle).
|
|
49
|
+
if (isRateLimited(session)) {
|
|
50
|
+
const until = session.rateLimitedUntil
|
|
51
|
+
? new Date(session.rateLimitedUntil).toISOString()
|
|
52
|
+
: "unknown time";
|
|
53
|
+
throw new Error(`${method} ${path} -> 401 (token rejected, but login.migros.ch is rate-limited until ${until} — cannot refresh)`);
|
|
54
|
+
}
|
|
44
55
|
try {
|
|
45
|
-
session = loadSession();
|
|
46
56
|
jwt = await silentOAuth(session);
|
|
47
57
|
saveSession(session);
|
|
48
58
|
r = await makeRequest(method, path, body, jwt, language, session);
|
|
@@ -50,6 +60,13 @@ export async function api(method, path, body, opts = {}) {
|
|
|
50
60
|
saveSession(session);
|
|
51
61
|
}
|
|
52
62
|
catch (e) {
|
|
63
|
+
// Rate-limit errors should always surface — swallowing them hides the
|
|
64
|
+
// cooldown from the caller and defeats the cascade-prevention logic.
|
|
65
|
+
if (e instanceof RateLimitError) {
|
|
66
|
+
// Save the cooldown silentOAuth just set, so the next call doesn't retry.
|
|
67
|
+
saveSession(session);
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
53
70
|
// Silent refresh failed (cookies actually expired). Surface the original
|
|
54
71
|
// 401 since that's more actionable than the silent-refresh error.
|
|
55
72
|
}
|
package/dist/auth/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/auth/api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAgB,MAAM,cAAc,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/auth/api.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/cookies.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface Session {
|
|
|
11
11
|
cookies: Record<string, Record<string, string>>;
|
|
12
12
|
jwt: string | null;
|
|
13
13
|
jwtExp: number;
|
|
14
|
+
/**
|
|
15
|
+
* Unix epoch **milliseconds** when the login.migros.ch rate-limit cooldown
|
|
16
|
+
* ends. Absent or 0 means the host is not known to be rate-limiting.
|
|
17
|
+
*/
|
|
18
|
+
rateLimitedUntil?: number;
|
|
19
|
+
/** Human-readable reason for the cooldown (carried from the 429 response). */
|
|
20
|
+
rateLimitReason?: string;
|
|
21
|
+
/** Consecutive 429s so far, used to step up the fallback backoff. */
|
|
22
|
+
rateLimitAttempts?: number;
|
|
23
|
+
/** The last full login hit a 2FA step. Not a secret, just a yes/no. */
|
|
24
|
+
twoFactorRequired?: boolean;
|
|
14
25
|
}
|
|
15
26
|
/** Read the persisted session, or return an empty one if no file exists. */
|
|
16
27
|
export declare function loadSession(): Session;
|
package/dist/auth/cookies.js
CHANGED
|
@@ -10,6 +10,10 @@ export function loadSession() {
|
|
|
10
10
|
cookies: parsed.cookies ?? {},
|
|
11
11
|
jwt: parsed.jwt ?? null,
|
|
12
12
|
jwtExp: parsed.jwtExp ?? 0,
|
|
13
|
+
rateLimitedUntil: parsed.rateLimitedUntil,
|
|
14
|
+
rateLimitReason: parsed.rateLimitReason,
|
|
15
|
+
rateLimitAttempts: parsed.rateLimitAttempts,
|
|
16
|
+
twoFactorRequired: parsed.twoFactorRequired,
|
|
13
17
|
};
|
|
14
18
|
}
|
|
15
19
|
catch (err) {
|
package/dist/auth/cookies.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/auth/cookies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/auth/cookies.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -5,13 +5,30 @@ export type { Session, Credentials };
|
|
|
5
5
|
export { silentOAuth, fullLogin };
|
|
6
6
|
/**
|
|
7
7
|
* Returns a usable access token, doing whatever is needed:
|
|
8
|
-
* 1. Use the cached JWT if it's still valid (with a
|
|
8
|
+
* 1. Use the cached JWT if it's still valid (with a 5 min buffer).
|
|
9
9
|
* 2. Else run silent OAuth using cached SSO cookies.
|
|
10
|
-
* 3. Else fall back to a full credentialed login (email
|
|
10
|
+
* 3. Else fall back to a full credentialed login (email → password → TOTP).
|
|
11
|
+
*
|
|
12
|
+
* **Rate-limit handling (429 Too Many Requests):**
|
|
13
|
+
*
|
|
14
|
+
* - If the session carries an active rate-limit cooldown (`rateLimitedUntil` in
|
|
15
|
+
* the future), we **never** attempt silent OAuth or full login. Instead we
|
|
16
|
+
* fail‑open when the cached JWT is still valid or throw a clear error.
|
|
17
|
+
* - A 429 from silent OAuth is **not** cascaded into full login because they
|
|
18
|
+
* share the same host (`login.migros.ch`) and the same Cloudflare throttle.
|
|
19
|
+
* - After a successful refresh the cooldown is cleared automatically.
|
|
11
20
|
*
|
|
12
21
|
* Persists the session to disk after any refresh so subsequent calls are fast.
|
|
13
22
|
*
|
|
14
23
|
* Throws if `creds` is missing AND no cookies are cached (first-time install
|
|
15
24
|
* with no env vars set).
|
|
16
25
|
*/
|
|
17
|
-
export
|
|
26
|
+
export interface GetJwtOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Try a full login even when the account is known to need a 2FA code and no
|
|
29
|
+
* seed or code was given. Set by the `authenticate` tool, so an account that
|
|
30
|
+
* has since turned 2FA off can still log in.
|
|
31
|
+
*/
|
|
32
|
+
tryLoginWithoutCode?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function getJwt(creds?: Credentials, opts?: GetJwtOptions): Promise<string>;
|
package/dist/auth/index.js
CHANGED
|
@@ -1,36 +1,90 @@
|
|
|
1
1
|
import { loadSession, saveSession } from "./cookies.js";
|
|
2
|
-
import { getJwt as getJwtFromOauth, silentOAuth } from "./oauth.js";
|
|
3
|
-
import { fullLogin } from "./login.js";
|
|
2
|
+
import { getJwt as getJwtFromOauth, silentOAuth, JWT_BUFFER_SECONDS } from "./oauth.js";
|
|
3
|
+
import { fullLogin, FreshCodeRequiredError } from "./login.js";
|
|
4
|
+
import { isRateLimited, RateLimitError, setRateLimited, clearRateLimit, rateLimitBlockedMessage, } from "./rate-limit.js";
|
|
4
5
|
export { silentOAuth, fullLogin };
|
|
5
|
-
|
|
6
|
-
* Returns a usable access token, doing whatever is needed:
|
|
7
|
-
* 1. Use the cached JWT if it's still valid (with a 60s buffer).
|
|
8
|
-
* 2. Else run silent OAuth using cached SSO cookies.
|
|
9
|
-
* 3. Else fall back to a full credentialed login (email -> password -> TOTP).
|
|
10
|
-
*
|
|
11
|
-
* Persists the session to disk after any refresh so subsequent calls are fast.
|
|
12
|
-
*
|
|
13
|
-
* Throws if `creds` is missing AND no cookies are cached (first-time install
|
|
14
|
-
* with no env vars set).
|
|
15
|
-
*/
|
|
16
|
-
export async function getJwt(creds) {
|
|
6
|
+
export async function getJwt(creds, opts = {}) {
|
|
17
7
|
const session = loadSession();
|
|
18
|
-
//
|
|
8
|
+
// --- Rate-limit cooldown guard ---
|
|
9
|
+
if (isRateLimited(session)) {
|
|
10
|
+
// Fail‑open: if the cached JWT is still valid we return it regardless of
|
|
11
|
+
// the throttle — a working session must never be blocked by the login rate
|
|
12
|
+
// limiter. Apply the same buffer as the refresh path so we never hand a
|
|
13
|
+
// near-expired token to the API (it would 401 and trigger a rate-limited
|
|
14
|
+
// refresh attempt).
|
|
15
|
+
const now = Math.floor(Date.now() / 1000);
|
|
16
|
+
if (session.jwt && session.jwtExp && session.jwtExp - now > JWT_BUFFER_SECONDS) {
|
|
17
|
+
return session.jwt;
|
|
18
|
+
}
|
|
19
|
+
// JWT expired + rate‑limited → hard fail with an actionable message.
|
|
20
|
+
throw new Error(rateLimitBlockedMessage(session));
|
|
21
|
+
}
|
|
22
|
+
// --- Path 1+2: try cached JWT, fall through to silent OAuth ---
|
|
19
23
|
try {
|
|
20
24
|
const jwt = await getJwtFromOauth(session);
|
|
25
|
+
// Successful refresh clears any stale cooldown.
|
|
26
|
+
clearRateLimit(session);
|
|
21
27
|
saveSession(session);
|
|
22
28
|
return jwt;
|
|
23
29
|
}
|
|
24
30
|
catch (e) {
|
|
25
|
-
|
|
31
|
+
if (e instanceof RateLimitError) {
|
|
32
|
+
// 429 from silent OAuth → apply cooldown, persist, and do NOT cascade.
|
|
33
|
+
// The throwing call already set the cooldown. Only set it here if it didn't,
|
|
34
|
+
// so one 429 isn't counted twice by the backoff.
|
|
35
|
+
if (!isRateLimited(session))
|
|
36
|
+
setRateLimited(session, e.retryAfterSeconds, e.message);
|
|
37
|
+
saveSession(session);
|
|
38
|
+
// Fail‑open: if the current (possibly stale) JWT is still valid we
|
|
39
|
+
// return it rather than failing the tool call entirely. Apply the same
|
|
40
|
+
// buffer as the refresh path (see above).
|
|
41
|
+
const now = Math.floor(Date.now() / 1000);
|
|
42
|
+
if (session.jwt && session.jwtExp && session.jwtExp - now > JWT_BUFFER_SECONDS) {
|
|
43
|
+
return session.jwt;
|
|
44
|
+
}
|
|
45
|
+
// No usable JWT — surface the rate-limit error.
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
// Not a rate-limit error — silent OAuth failed because cookies are
|
|
49
|
+
// missing/expired. Fall through to a full credentialed login.
|
|
26
50
|
if (!creds) {
|
|
27
51
|
throw new Error("no cached session and no credentials provided. " +
|
|
28
|
-
"Set MIGROS_EMAIL
|
|
52
|
+
"Set MIGROS_EMAIL and MIGROS_PASSWORD to enable login. If the account has 2FA, also set " +
|
|
53
|
+
"MIGROS_TOTP_SECRET, or call authenticate with the current six-digit code.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Don't start a login that can't finish: if the last login needed a 2FA code
|
|
57
|
+
// and there is neither a stored seed nor a code, ask for the code before
|
|
58
|
+
// sending the password to Migros. Repeated half-logins get the IP blocked.
|
|
59
|
+
if (session.twoFactorRequired && !creds.totpSecret && !creds.totpCode && !opts.tryLoginWithoutCode) {
|
|
60
|
+
throw new FreshCodeRequiredError();
|
|
61
|
+
}
|
|
62
|
+
// --- Path 3: full credentialed login ---
|
|
63
|
+
try {
|
|
64
|
+
const jwt = await fullLogin(session, creds);
|
|
65
|
+
// Successful login clears any stale cooldown.
|
|
66
|
+
clearRateLimit(session);
|
|
67
|
+
saveSession(session);
|
|
68
|
+
return jwt;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// If fullLogin hits a 429, persist the cooldown so the next call doesn't
|
|
72
|
+
// immediately retry.
|
|
73
|
+
if (e instanceof RateLimitError) {
|
|
74
|
+
// The throwing call already set the cooldown. Only set it here if it didn't,
|
|
75
|
+
// so one 429 isn't counted twice by the backoff.
|
|
76
|
+
if (!isRateLimited(session))
|
|
77
|
+
setRateLimited(session, e.retryAfterSeconds, e.message);
|
|
78
|
+
saveSession(session);
|
|
79
|
+
}
|
|
80
|
+
if (e instanceof FreshCodeRequiredError) {
|
|
81
|
+
// Remember that this account needs a code. Start from the saved session
|
|
82
|
+
// so the half-finished login's cookies aren't kept.
|
|
83
|
+
const saved = loadSession();
|
|
84
|
+
saved.twoFactorRequired = true;
|
|
85
|
+
saveSession(saved);
|
|
29
86
|
}
|
|
87
|
+
throw e;
|
|
30
88
|
}
|
|
31
|
-
// Path 3: full credentialed login.
|
|
32
|
-
const jwt = await fullLogin(session, creds);
|
|
33
|
-
saveSession(session);
|
|
34
|
-
return jwt;
|
|
35
89
|
}
|
|
36
90
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAgB,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/login.d.ts
CHANGED
|
@@ -2,14 +2,32 @@ import type { Session } from "./cookies.js";
|
|
|
2
2
|
export interface Credentials {
|
|
3
3
|
email: string;
|
|
4
4
|
password: string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Base32-encoded TOTP seed (MIGROS_TOTP_SECRET). When set, the MCP makes the
|
|
7
|
+
* 2FA code itself and re-logins need no help.
|
|
8
|
+
*/
|
|
6
9
|
totpSecret?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Current six-digit TOTP code passed to the `authenticate` tool. The option
|
|
12
|
+
* for people who don't want to store the seed. Used before the seed if both
|
|
13
|
+
* are present.
|
|
14
|
+
*/
|
|
15
|
+
totpCode?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class FreshCodeRequiredError extends Error {
|
|
18
|
+
constructor(message?: string);
|
|
7
19
|
}
|
|
8
20
|
/**
|
|
9
21
|
* RFC 6238 TOTP code from a base32-encoded shared secret.
|
|
10
22
|
* Defaults match Google Authenticator / 1Password / Authy: SHA-1, 30s, 6 digits.
|
|
11
23
|
*/
|
|
12
|
-
export declare function
|
|
24
|
+
export declare function totpFromSecret(secretBase32: string, time?: number, step?: number, digits?: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* The code to submit at the 2FA step: the one passed to `authenticate` if
|
|
27
|
+
* there is one, else one made from the stored seed. Throws
|
|
28
|
+
* FreshCodeRequiredError when there is neither.
|
|
29
|
+
*/
|
|
30
|
+
export declare function otpCodeFor(creds: Credentials, time?: number): string;
|
|
13
31
|
/**
|
|
14
32
|
* Run the full credentialed login (email → password → TOTP) and finish by
|
|
15
33
|
* minting a JWT via silent OAuth. Mutates `session` in place. Caller persists.
|
package/dist/auth/login.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { ingestSetCookie } from "./cookies.js";
|
|
3
3
|
import { client, uniformHeaders, silentOAuth } from "./oauth.js";
|
|
4
|
+
import { parseRetryAfter, RateLimitError, setRateLimited } from "./rate-limit.js";
|
|
4
5
|
const LOGIN = "https://login.migros.ch";
|
|
6
|
+
export class FreshCodeRequiredError extends Error {
|
|
7
|
+
constructor(message = "fresh_code_required: this Migros account needs a 2FA code. Ask the user for the current " +
|
|
8
|
+
"six-digit code from their authenticator app and call the authenticate tool with it.") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "FreshCodeRequiredError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
5
13
|
/**
|
|
6
14
|
* RFC 6238 TOTP code from a base32-encoded shared secret.
|
|
7
15
|
* Defaults match Google Authenticator / 1Password / Authy: SHA-1, 30s, 6 digits.
|
|
8
16
|
*/
|
|
9
|
-
export function
|
|
17
|
+
export function totpFromSecret(secretBase32, time = Math.floor(Date.now() / 1000), step = 30, digits = 6) {
|
|
10
18
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
11
19
|
const cleaned = secretBase32.replace(/=+$/g, "").replace(/\s+/g, "").toUpperCase();
|
|
12
20
|
let bits = "";
|
|
@@ -27,6 +35,21 @@ export function totpCode(secretBase32, time = Math.floor(Date.now() / 1000), ste
|
|
|
27
35
|
const slice = hmac.readUInt32BE(offset) & 0x7fffffff;
|
|
28
36
|
return String(slice % 10 ** digits).padStart(digits, "0");
|
|
29
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The code to submit at the 2FA step: the one passed to `authenticate` if
|
|
40
|
+
* there is one, else one made from the stored seed. Throws
|
|
41
|
+
* FreshCodeRequiredError when there is neither.
|
|
42
|
+
*/
|
|
43
|
+
export function otpCodeFor(creds, time) {
|
|
44
|
+
if (creds.totpCode) {
|
|
45
|
+
if (!/^\d{6}$/.test(creds.totpCode))
|
|
46
|
+
throw new Error("totpCode must be exactly six digits");
|
|
47
|
+
return creds.totpCode;
|
|
48
|
+
}
|
|
49
|
+
if (creds.totpSecret)
|
|
50
|
+
return totpFromSecret(creds.totpSecret, time);
|
|
51
|
+
throw new FreshCodeRequiredError();
|
|
52
|
+
}
|
|
30
53
|
function findCsrf(html) {
|
|
31
54
|
const m = html.match(/name="_csrf"[^>]*value="([^"]+)"/);
|
|
32
55
|
if (!m)
|
|
@@ -42,6 +65,12 @@ async function get(url, session, extra = {}) {
|
|
|
42
65
|
const host = new URL(url).host;
|
|
43
66
|
const r = await client().get(url, { headers: uniformHeaders(host, session, { Accept: "text/html", ...extra }) });
|
|
44
67
|
ingestSetCookie(session, host, r.headers["set-cookie"]);
|
|
68
|
+
if (r.status === 429) {
|
|
69
|
+
const retryAfter = parseRetryAfter(r.headers["retry-after"]);
|
|
70
|
+
setRateLimited(session, retryAfter, "login " + url + " -> 429");
|
|
71
|
+
throw new RateLimitError("login.migros.ch rate-limited (429) during GET " + url + ". " +
|
|
72
|
+
"Retry after: " + (retryAfter ?? "unknown") + "s", retryAfter);
|
|
73
|
+
}
|
|
45
74
|
return r;
|
|
46
75
|
}
|
|
47
76
|
async function postForm(url, session, body) {
|
|
@@ -58,6 +87,12 @@ async function postForm(url, session, body) {
|
|
|
58
87
|
}),
|
|
59
88
|
});
|
|
60
89
|
ingestSetCookie(session, host, r.headers["set-cookie"]);
|
|
90
|
+
if (r.status === 429) {
|
|
91
|
+
const retryAfter = parseRetryAfter(r.headers["retry-after"]);
|
|
92
|
+
setRateLimited(session, retryAfter, "login POST " + url + " -> 429");
|
|
93
|
+
throw new RateLimitError("login.migros.ch rate-limited (429) during POST " + url + ". " +
|
|
94
|
+
"Retry after: " + (retryAfter ?? "unknown") + "s", retryAfter);
|
|
95
|
+
}
|
|
61
96
|
return r;
|
|
62
97
|
}
|
|
63
98
|
function locationOf(r, base) {
|
|
@@ -109,17 +144,25 @@ export async function fullLogin(session, creds) {
|
|
|
109
144
|
nextUrl = `${LOGIN}/login/authenticator`;
|
|
110
145
|
// 7. If a 2FA step is in the chain, handle it. Otherwise skip.
|
|
111
146
|
const needsOtp = /\/login\/(authenticator|second|totp|otp)/.test(nextUrl);
|
|
147
|
+
// Remembered in the session so a later login without seed or code can stop
|
|
148
|
+
// before sending the password (see getJwt).
|
|
149
|
+
session.twoFactorRequired = needsOtp;
|
|
112
150
|
if (needsOtp) {
|
|
113
|
-
|
|
114
|
-
throw new Error("server requires 2FA but no TOTP secret was provided");
|
|
151
|
+
const code = otpCodeFor(creds);
|
|
115
152
|
r = await get(nextUrl, session);
|
|
116
153
|
csrf = findCsrf(r.data);
|
|
117
154
|
const otpAction = r.data.match(/<form[^>]+action="([^"]*)"/)?.[1] || nextUrl;
|
|
118
155
|
const otpUrl = new URL(otpAction, nextUrl).toString();
|
|
119
|
-
r = await postForm(otpUrl, session, { _csrf: csrf, code
|
|
156
|
+
r = await postForm(otpUrl, session, { _csrf: csrf, code });
|
|
120
157
|
nextUrl = locationOf(r, LOGIN);
|
|
121
158
|
if (!nextUrl) {
|
|
122
|
-
|
|
159
|
+
const why = findError(r.data) ?? `status ${r.status}`;
|
|
160
|
+
if (creds.totpCode) {
|
|
161
|
+
// A code typed in or read from a password manager may simply have
|
|
162
|
+
// expired: ask for a new one, and keep the real reason in the message.
|
|
163
|
+
throw new FreshCodeRequiredError(`fresh_code_required: Migros did not accept the code (${why}). Get a new code and call authenticate once more.`);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`OTP step failed: ${why}`);
|
|
123
166
|
}
|
|
124
167
|
}
|
|
125
168
|
// 8. Follow the post-login redirect chain to settle SSO state.
|
package/dist/auth/login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AxiosInstance } from "axios";
|
|
2
2
|
import type { Session } from "./cookies.js";
|
|
3
3
|
export declare function client(): AxiosInstance;
|
|
4
|
-
export declare function uaFor(host: string): string;
|
|
5
4
|
export declare function uniformHeaders(host: string, session: Session, extra?: Record<string, string>): Record<string, string>;
|
|
6
5
|
/**
|
|
7
6
|
* Run the silent OAuth flow against current SSO cookies and return a fresh
|
|
@@ -10,9 +9,20 @@ export declare function uniformHeaders(host: string, session: Session, extra?: R
|
|
|
10
9
|
* Throws if the SSO cookies are missing/expired (e.g., user logged out of
|
|
11
10
|
* migros.ch in their browser, or session.json is stale by weeks).
|
|
12
11
|
*
|
|
12
|
+
* On a 429 (rate-limit), sets `session.rateLimitedUntil` and throws a
|
|
13
|
+
* {@link RateLimitError} so the caller can surface the cooldown and avoid
|
|
14
|
+
* cascading into a full credentialed login.
|
|
15
|
+
*
|
|
13
16
|
* Caller is responsible for persisting the session afterward.
|
|
14
17
|
*/
|
|
15
18
|
export declare function silentOAuth(session: Session): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* How much JWT lifetime (in seconds) we keep in reserve before proactively
|
|
21
|
+
* refreshing via silent OAuth. Also used by fail-open paths in getJwt() so a
|
|
22
|
+
* near-expired token is never handed to the API (which would 401 and trigger
|
|
23
|
+
* a rate-limited refresh attempt).
|
|
24
|
+
*/
|
|
25
|
+
export declare const JWT_BUFFER_SECONDS = 300;
|
|
16
26
|
/**
|
|
17
27
|
* Returns a usable JWT, refreshing via silent OAuth if the cached one is
|
|
18
28
|
* within `bufferSeconds` of expiring (or missing). Mutates `session`.
|