migros-mcp 0.2.0 → 0.3.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.
Files changed (56) hide show
  1. package/README.md +108 -24
  2. package/dist/auth/api.d.ts +19 -0
  3. package/dist/auth/api.js +63 -0
  4. package/dist/auth/api.js.map +1 -0
  5. package/dist/auth/cookies.d.ts +25 -0
  6. package/dist/auth/cookies.js +66 -0
  7. package/dist/auth/cookies.js.map +1 -0
  8. package/dist/auth/index.d.ts +17 -0
  9. package/dist/auth/index.js +36 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/login.d.ts +20 -0
  12. package/dist/auth/login.js +138 -0
  13. package/dist/auth/login.js.map +1 -0
  14. package/dist/auth/oauth.d.ts +20 -0
  15. package/dist/auth/oauth.js +116 -0
  16. package/dist/auth/oauth.js.map +1 -0
  17. package/dist/auth/paths.d.ts +11 -0
  18. package/dist/auth/paths.js +26 -0
  19. package/dist/auth/paths.js.map +1 -0
  20. package/dist/auth-tools/_shared.d.ts +6 -0
  21. package/dist/auth-tools/_shared.js +12 -0
  22. package/dist/auth-tools/_shared.js.map +1 -0
  23. package/dist/auth-tools/account.d.ts +9 -0
  24. package/dist/auth-tools/account.js +35 -0
  25. package/dist/auth-tools/account.js.map +1 -0
  26. package/dist/auth-tools/cart.d.ts +37 -0
  27. package/dist/auth-tools/cart.js +118 -0
  28. package/dist/auth-tools/cart.js.map +1 -0
  29. package/dist/auth-tools/cumulus.d.ts +31 -0
  30. package/dist/auth-tools/cumulus.js +83 -0
  31. package/dist/auth-tools/cumulus.js.map +1 -0
  32. package/dist/auth-tools/orders.d.ts +15 -0
  33. package/dist/auth-tools/orders.js +32 -0
  34. package/dist/auth-tools/orders.js.map +1 -0
  35. package/dist/auth-tools.d.ts +9 -0
  36. package/dist/auth-tools.js +10 -0
  37. package/dist/auth-tools.js.map +1 -0
  38. package/dist/index.d.ts +0 -0
  39. package/dist/index.js +37 -80
  40. package/dist/index.js.map +1 -1
  41. package/dist/migros.d.ts +17 -0
  42. package/dist/migros.js +42 -1
  43. package/dist/migros.js.map +1 -1
  44. package/dist/register.d.ts +11 -0
  45. package/dist/register.js +23 -0
  46. package/dist/register.js.map +1 -0
  47. package/dist/schemas.d.ts +125 -0
  48. package/dist/schemas.js +82 -12
  49. package/dist/schemas.js.map +1 -1
  50. package/dist/tools.d.ts +17 -0
  51. package/dist/tools.js +48 -1
  52. package/dist/tools.js.map +1 -1
  53. package/dist/types.d.ts +0 -0
  54. package/dist/types.js +0 -0
  55. package/dist/types.js.map +0 -0
  56. package/package.json +3 -2
package/README.md CHANGED
@@ -1,92 +1,175 @@
1
1
  # migros-mcp
2
2
 
3
- Search [Migros](https://www.migros.ch) products, get nutrition facts, find stores, and browse promotions with Claude.
3
+ Search [Migros](https://www.migros.ch) products and (optionally) manage your basket, addresses, and orders through Claude.
4
4
 
5
5
  > **Important**
6
6
  > This is **not affiliated with Migros** in any way.
7
- > It uses publicly accessible API endpoints -- there is no official Migros API.
7
+ > It uses publicly accessible API endpoints. There is no official Migros API.
8
8
  > It can **break at any time** if Migros changes their endpoints.
9
- > No authentication required -- uses guest access only.
10
9
  > **Use at your own risk.**
11
10
 
12
11
  ## What you can do
13
12
 
14
- Once installed, just talk to Claude naturally:
15
-
13
+ **Without an account (anonymous access):**
16
14
  - "Search Migros for protein bars"
17
15
  - "What's the nutrition info for this Migros product?"
18
16
  - "Find Migros stores near Zurich"
19
17
  - "What promotions does Migros have right now?"
20
18
  - "Compare the protein content of these two products"
21
- - "Which Migros products are gluten-free?"
19
+ - "Find me a recipe for risotto and add the ingredients to my basket" (combines anonymous recipe search with authenticated cart write)
20
+
21
+ **With your Migros account (optional):**
22
+ - "What's currently in my Migros basket?"
23
+ - "Add 2 liters of milk to my basket"
24
+ - "Remove the bananas from my basket"
25
+ - "Where will my order be delivered to?"
26
+ - "Show me my last 5 Migros orders"
27
+ - "Give me the link to finish my checkout"
28
+ - "How much did I spend at Migros last month?"
29
+ - "What was on my last in-store receipt?"
30
+ - "Which Cumulus coupons do I have right now?"
31
+ - "How many Cumulus points do I have?"
32
+
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.
22
34
 
23
35
  ## Install
24
36
 
25
- No accounts or credentials needed. This server uses Migros guest access.
26
-
27
37
  ### Claude Desktop (recommended)
28
38
 
29
39
  1. Download [migros-mcp.mcpb](https://github.com/lewpgs/migros-mcp/releases/latest/download/migros-mcp.mcpb)
30
40
  2. Install it:
31
41
 
32
- **macOS** - Double-click the file, or drag and drop it onto the Claude Desktop app icon
42
+ **macOS** — Double-click the file, or drag and drop it onto the Claude Desktop app icon
43
+
44
+ **Windows** — In Claude Desktop, go to File > Settings > Extensions > Advanced Settings > Install Extension and select the file
45
+
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
+
48
+ **About the TOTP secret.** This is **NOT** the rotating 6-digit code your authenticator app shows. It is the **static base32 seed** given to you (or scanned via QR code) when you originally set up the authenticator. It's a one-time value, typically ~32 characters of `A-Z2-7` (for example: `JBSWY3DPEHPK3PXP3PXP3PXP3PXP3PXP`), and it doesn't change every 30 seconds.
49
+
50
+ How to find it:
33
51
 
34
- **Windows** - In Claude Desktop, go to File > Settings > Extensions > Advanced Settings > Install Extension and select the file
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.
35
54
 
36
- That's it. No credentials needed.
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
+
57
+ Leave the field blank entirely if your Migros account doesn't have 2FA enabled.
58
+
59
+ That's it.
37
60
 
38
61
  ---
39
62
 
40
63
  ## Advanced setup
41
64
 
42
- These methods require [Node.js 18+](https://nodejs.org) installed on your machine.
65
+ These methods require [Node.js 18+](https://nodejs.org).
43
66
 
44
67
  ### Claude Code
45
68
 
46
69
  ```bash
70
+ # Anonymous (search/browse only)
47
71
  claude mcp add migros -- npx -y migros-mcp
72
+
73
+ # With your account (basket + orders)
74
+ claude mcp add migros \
75
+ -e MIGROS_EMAIL=you@example.com \
76
+ -e MIGROS_PASSWORD='your-password' \
77
+ -e MIGROS_TOTP_SECRET=ABCDEFGHIJKLMNOP \
78
+ -- npx -y migros-mcp
48
79
  ```
49
80
 
50
81
  ### Claude Desktop (manual) / Cursor
51
82
 
52
- Add the following to your config file:
83
+ Add to your config:
53
84
 
54
- - **Claude Desktop** - `claude_desktop_config.json`
55
- - **Cursor** - `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
85
+ - **Claude Desktop** — `claude_desktop_config.json`
86
+ - **Cursor** — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
56
87
 
57
88
  ```json
58
89
  {
59
90
  "mcpServers": {
60
91
  "migros": {
61
92
  "command": "npx",
62
- "args": ["-y", "migros-mcp"]
93
+ "args": ["-y", "migros-mcp"],
94
+ "env": {
95
+ "MIGROS_EMAIL": "you@example.com",
96
+ "MIGROS_PASSWORD": "your-password",
97
+ "MIGROS_TOTP_SECRET": "ABCDEFGHIJKLMNOP"
98
+ }
63
99
  }
64
100
  }
65
101
  }
66
102
  ```
67
103
 
104
+ The `env` block is optional. Omit it to use anonymous tools only.
105
+
68
106
  ### Test connection
69
107
 
70
108
  ```bash
71
- npx migros-mcp
109
+ npx migros-mcp # anonymous
110
+ MIGROS_EMAIL=... MIGROS_PASSWORD=... MIGROS_TOTP_SECRET=... npx migros-mcp # authenticated
72
111
  ```
73
112
 
74
113
  ## Available tools
75
114
 
115
+ ### Anonymous (no credentials needed)
116
+
76
117
  | Tool | Description |
77
118
  |---|---|
78
119
  | `search_products` | Search Migros products by name or keyword. Returns product IDs. |
79
- | `get_product_details` | Get full product info: nutrition, ingredients, allergens, price, ratings. |
120
+ | `get_product_details` | Full product info: nutrition, ingredients, allergens, price, ratings. |
80
121
  | `get_stock` | Check product availability at a specific Migros store. Updated once daily. |
81
122
  | `get_categories` | List all Migros product categories. |
82
123
  | `search_stores` | Find Migros stores by location with addresses and opening hours. |
83
124
  | `get_promotions` | Search current Migros promotions and deals. |
125
+ | `search_recipes` | Search Migusto (Migros' recipe site) by query and/or ingredients. German/French/Italian only. |
126
+ | `get_recipe_details` | Full recipe by slug: ingredients, instructions, photos, nutrition. |
127
+ | `get_recipe_products` | Migros product UIDs for a recipe — feed directly into `add_to_basket`. |
128
+
129
+ ### Authenticated (require credentials)
130
+
131
+ | Tool | Description |
132
+ |---|---|
133
+ | `get_profile` | Logged-in customer's basic profile (name, email, language). |
134
+ | `get_addresses` | Saved delivery and billing addresses. |
135
+ | `get_basket` | Items currently in the user's basket. |
136
+ | `add_to_basket` | Add a product (sets target quantity, default 1). |
137
+ | `update_basket_quantity` | Set an item's exact quantity (0 to remove). |
138
+ | `remove_from_basket` | Remove an item entirely. |
139
+ | `get_orders` | List your online orders, paginated, filterable by status. |
140
+ | `get_order_details` | Full details of a single order by id. |
141
+ | `get_checkout_link` | Returns the URL to open in your browser to complete checkout. |
142
+ | `get_cumulus_status` | Cumulus loyalty card status: points balance, level, cardholder. |
143
+ | `get_in_store_receipts` | List in-store receipts (Kassenbons) over a date range. |
144
+ | `get_receipt_details` | URL to view a single in-store receipt's line items in the browser. |
145
+ | `get_cumulus_coupons` | Active personalized Cumulus coupons. |
84
146
 
85
147
  ## How it works
86
148
 
87
- Migros does not offer a public API. This MCP server uses the [migros-api-wrapper](https://www.npmjs.com/package/migros-api-wrapper) package, which wraps the same endpoints that the Migros website and app use internally. A guest token is obtained at startup (no login required) and cached for the session.
149
+ ### Anonymous tools
150
+ 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.
151
+
152
+ ### Authenticated tools
153
+ 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 — no login form re-submission.
154
+
155
+ 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.
156
+
157
+ The session lives at:
158
+
159
+ - macOS: `~/Library/Application Support/migros-mcp/session.json`
160
+ - Linux: `$XDG_CONFIG_HOME/migros-mcp/session.json`
161
+ - Windows: `%APPDATA%/migros-mcp/session.json`
162
+
163
+ If the cached session expires (typically a few weeks), the MCP automatically re-runs the credentialed login.
164
+
165
+ ### Order placement
166
+ 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.
167
+
168
+ ## Known limitations
88
169
 
89
- Product searches return IDs that you then pass to `get_product_details` for full nutrition tables, ingredients, allergens, and pricing.
170
+ - **2FA support is TOTP-only.** Accounts secured with a passkey (and no TOTP fallback) are not supported in v0.3.0. 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.
171
+ - **No automatic order placement.** See above — `get_checkout_link` hands off to your browser for the actual placement.
172
+ - **Cloudflare rate limits.** If the MCP fails the credentialed login repeatedly in a short window (e.g., wrong password retries), Cloudflare may briefly throttle the IP. Wait an hour and retry, or log in via your browser to refresh the session.
90
173
 
91
174
  ## Development
92
175
 
@@ -106,10 +189,11 @@ npx -y @modelcontextprotocol/inspector npx migros-mcp
106
189
 
107
190
  ## Disclaimers
108
191
 
109
- - **Unofficial** - This project is not affiliated with, endorsed by, or connected to Migros in any way.
110
- - **No official API** - This server uses publicly accessible API endpoints that power the Migros website. These are not documented or officially supported. Changes to Migros infrastructure could break this server without notice.
111
- - **No authentication required** - Uses guest access only. No credentials are stored or transmitted.
112
- - **Use at your own risk** - No guarantees of functionality, availability, or compatibility.
192
+ - **Unofficial** — Not affiliated with, endorsed by, or connected to Migros in any way.
193
+ - **No official API** — Uses endpoints that power the Migros website. They are not documented or supported. Migros can change them without notice.
194
+ - **Credentials** — When you provide credentials, they're used only to authenticate against `login.migros.ch`. Resulting session cookies and access tokens are stored locally in your OS user config directory. They are never sent anywhere except Migros' own servers.
195
+ - **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`.
196
+ - **Use at your own risk** — No guarantees of functionality, availability, or compatibility.
113
197
 
114
198
  ## License
115
199
 
@@ -0,0 +1,19 @@
1
+ import type { Credentials } from "./login.js";
2
+ export interface AuthOptions {
3
+ /** Credentials used only when no cached session exists. */
4
+ creds?: Credentials;
5
+ /** Override the user's locale for migros-language / accept-language headers. */
6
+ language?: string;
7
+ }
8
+ /**
9
+ * Make an authenticated request against any www.migros.ch path. Reuses the
10
+ * persisted session, refreshes the JWT silently when needed, and falls back
11
+ * to a full credentialed login if cookies have expired.
12
+ *
13
+ * On 401 (token rejected mid-request — e.g., user changed their password,
14
+ * or the server invalidated the token early for any reason), forces a fresh
15
+ * silent OAuth and retries once before giving up.
16
+ *
17
+ * Returns the parsed response body. Throws with a descriptive error on non-2xx.
18
+ */
19
+ export declare function api(method: "GET" | "POST" | "PUT" | "DELETE", path: string, body?: unknown, opts?: AuthOptions): Promise<unknown>;
@@ -0,0 +1,63 @@
1
+ import { client, silentOAuth, uniformHeaders } from "./oauth.js";
2
+ import { ingestSetCookie, loadSession, saveSession } from "./cookies.js";
3
+ import { getJwt } from "./index.js";
4
+ const BASE = "https://www.migros.ch";
5
+ async function makeRequest(method, path, body, jwt, language, session) {
6
+ const headers = uniformHeaders("www.migros.ch", session, {
7
+ Accept: "application/json, text/plain, */*",
8
+ Authorization: `Bearer ${jwt}`,
9
+ "migros-language": language,
10
+ "accept-language": language,
11
+ "peer-id": "website-js-1143.0.0",
12
+ Origin: BASE,
13
+ Referer: `${BASE}/en`,
14
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
15
+ });
16
+ return client().request({
17
+ method,
18
+ url: `${BASE}${path}`,
19
+ data: body,
20
+ headers,
21
+ });
22
+ }
23
+ /**
24
+ * Make an authenticated request against any www.migros.ch path. Reuses the
25
+ * persisted session, refreshes the JWT silently when needed, and falls back
26
+ * to a full credentialed login if cookies have expired.
27
+ *
28
+ * On 401 (token rejected mid-request — e.g., user changed their password,
29
+ * or the server invalidated the token early for any reason), forces a fresh
30
+ * silent OAuth and retries once before giving up.
31
+ *
32
+ * Returns the parsed response body. Throws with a descriptive error on non-2xx.
33
+ */
34
+ export async function api(method, path, body, opts = {}) {
35
+ const language = opts.language ?? "en";
36
+ // First attempt with current cached / refreshed JWT.
37
+ let jwt = await getJwt(opts.creds);
38
+ let session = loadSession();
39
+ let r = await makeRequest(method, path, body, jwt, language, session);
40
+ ingestSetCookie(session, "www.migros.ch", r.headers["set-cookie"]);
41
+ saveSession(session);
42
+ // Token rejected? Force a silent refresh and retry once.
43
+ if (r.status === 401) {
44
+ try {
45
+ session = loadSession();
46
+ jwt = await silentOAuth(session);
47
+ saveSession(session);
48
+ r = await makeRequest(method, path, body, jwt, language, session);
49
+ ingestSetCookie(session, "www.migros.ch", r.headers["set-cookie"]);
50
+ saveSession(session);
51
+ }
52
+ catch (e) {
53
+ // Silent refresh failed (cookies actually expired). Surface the original
54
+ // 401 since that's more actionable than the silent-refresh error.
55
+ }
56
+ }
57
+ if (r.status < 200 || r.status >= 300) {
58
+ const snippet = typeof r.data === "string" ? r.data.slice(0, 200) : JSON.stringify(r.data).slice(0, 200);
59
+ throw new Error(`${method} ${path} -> ${r.status}: ${snippet}`);
60
+ }
61
+ return typeof r.data === "string" ? JSON.parse(r.data) : r.data;
62
+ }
63
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/auth/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Persisted auth state. Lives in the OS config dir, not in the project tree.
3
+ *
4
+ * - `cookies` is keyed by host so we can send the right Cookie header per request.
5
+ * - `jwt` is the access token from /authentication/.../oauth/login-success (Bearer
6
+ * prefix already stripped by the caller).
7
+ * - `jwtExp` is the JWT's `exp` claim in unix seconds, used to decide when to
8
+ * refresh proactively without parsing the token on every call.
9
+ */
10
+ export interface Session {
11
+ cookies: Record<string, Record<string, string>>;
12
+ jwt: string | null;
13
+ jwtExp: number;
14
+ }
15
+ /** Read the persisted session, or return an empty one if no file exists. */
16
+ export declare function loadSession(): Session;
17
+ /** Write the session atomically (temp file + rename) to avoid partial writes. */
18
+ export declare function saveSession(session: Session): void;
19
+ /**
20
+ * Update the jar from a server's Set-Cookie response headers for a given host.
21
+ * Mutates `session.cookies[host]` in place. Caller decides when to persist.
22
+ */
23
+ export declare function ingestSetCookie(session: Session, host: string, setCookies: string | string[] | undefined): void;
24
+ /** Build the `Cookie:` request header for a host (empty string if no cookies). */
25
+ export declare function cookieHeader(session: Session, host: string): string;
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import { configDir, sessionFile } from "./paths.js";
3
+ const EMPTY = { cookies: {}, jwt: null, jwtExp: 0 };
4
+ /** Read the persisted session, or return an empty one if no file exists. */
5
+ export function loadSession() {
6
+ try {
7
+ const raw = fs.readFileSync(sessionFile(), "utf8");
8
+ const parsed = JSON.parse(raw);
9
+ return {
10
+ cookies: parsed.cookies ?? {},
11
+ jwt: parsed.jwt ?? null,
12
+ jwtExp: parsed.jwtExp ?? 0,
13
+ };
14
+ }
15
+ catch (err) {
16
+ // Missing file (ENOENT) or unreadable: treat as empty.
17
+ if (err.code === "ENOENT")
18
+ return { ...EMPTY, cookies: {} };
19
+ // Corrupted JSON: don't throw and crash the MCP — start fresh and let the user re-import.
20
+ return { ...EMPTY, cookies: {} };
21
+ }
22
+ }
23
+ /** Write the session atomically (temp file + rename) to avoid partial writes. */
24
+ export function saveSession(session) {
25
+ const dir = configDir();
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ const finalPath = sessionFile();
28
+ const tmpPath = `${finalPath}.${process.pid}.tmp`;
29
+ fs.writeFileSync(tmpPath, JSON.stringify(session, null, 2), { mode: 0o600 });
30
+ fs.renameSync(tmpPath, finalPath);
31
+ }
32
+ /**
33
+ * Update the jar from a server's Set-Cookie response headers for a given host.
34
+ * Mutates `session.cookies[host]` in place. Caller decides when to persist.
35
+ */
36
+ export function ingestSetCookie(session, host, setCookies) {
37
+ if (!setCookies)
38
+ return;
39
+ const list = Array.isArray(setCookies) ? setCookies : [setCookies];
40
+ const bag = (session.cookies[host] ??= {});
41
+ for (const sc of list) {
42
+ // Take only the "name=value" pair before the first semicolon. Attributes
43
+ // like Path, Domain, HttpOnly, etc. are intentionally discarded — we don't
44
+ // re-issue cookies, we just send what we got back to the same host.
45
+ const semi = sc.indexOf(";");
46
+ const pair = semi === -1 ? sc : sc.slice(0, semi);
47
+ const eq = pair.indexOf("=");
48
+ if (eq < 0)
49
+ continue;
50
+ const name = pair.slice(0, eq).trim();
51
+ const value = pair.slice(eq + 1).trim();
52
+ if (!name)
53
+ continue;
54
+ bag[name] = value;
55
+ }
56
+ }
57
+ /** Build the `Cookie:` request header for a host (empty string if no cookies). */
58
+ export function cookieHeader(session, host) {
59
+ const bag = session.cookies[host];
60
+ if (!bag)
61
+ return "";
62
+ return Object.entries(bag)
63
+ .map(([k, v]) => `${k}=${v}`)
64
+ .join("; ");
65
+ }
66
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/auth/cookies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type Session } from "./cookies.js";
2
+ import { silentOAuth } from "./oauth.js";
3
+ import { fullLogin, type Credentials } from "./login.js";
4
+ export type { Session, Credentials };
5
+ export { silentOAuth, fullLogin };
6
+ /**
7
+ * Returns a usable access token, doing whatever is needed:
8
+ * 1. Use the cached JWT if it's still valid (with a 60s buffer).
9
+ * 2. Else run silent OAuth using cached SSO cookies.
10
+ * 3. Else fall back to a full credentialed login (email -> password -> TOTP).
11
+ *
12
+ * Persists the session to disk after any refresh so subsequent calls are fast.
13
+ *
14
+ * Throws if `creds` is missing AND no cookies are cached (first-time install
15
+ * with no env vars set).
16
+ */
17
+ export declare function getJwt(creds?: Credentials): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { loadSession, saveSession } from "./cookies.js";
2
+ import { getJwt as getJwtFromOauth, silentOAuth } from "./oauth.js";
3
+ import { fullLogin } from "./login.js";
4
+ 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) {
17
+ const session = loadSession();
18
+ // Path 1+2: try cached JWT, fall through to silent OAuth.
19
+ try {
20
+ const jwt = await getJwtFromOauth(session);
21
+ saveSession(session);
22
+ return jwt;
23
+ }
24
+ catch (e) {
25
+ // Silent OAuth failed (no cookies / expired cookies). Fall through.
26
+ if (!creds) {
27
+ throw new Error("no cached session and no credentials provided. " +
28
+ "Set MIGROS_EMAIL, MIGROS_PASSWORD, and MIGROS_TOTP_SECRET env vars to enable login.");
29
+ }
30
+ }
31
+ // Path 3: full credentialed login.
32
+ const jwt = await fullLogin(session, creds);
33
+ saveSession(session);
34
+ return jwt;
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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;AACpE,OAAO,EAAE,SAAS,EAAoB,MAAM,YAAY,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AAElC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAmB;IAC9C,MAAM,OAAO,GAAY,WAAW,EAAE,CAAC;IAEvC,0DAA0D;IAC1D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC3C,WAAW,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,oEAAoE;QACpE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,iDAAiD;gBAC/C,qFAAqF,CACxF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5C,WAAW,CAAC,OAAO,CAAC,CAAC;IACrB,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Session } from "./cookies.js";
2
+ export interface Credentials {
3
+ email: string;
4
+ password: string;
5
+ /** Base32-encoded TOTP seed. Required if 2FA is enabled on the account. */
6
+ totpSecret?: string;
7
+ }
8
+ /**
9
+ * RFC 6238 TOTP code from a base32-encoded shared secret.
10
+ * Defaults match Google Authenticator / 1Password / Authy: SHA-1, 30s, 6 digits.
11
+ */
12
+ export declare function totpCode(secretBase32: string, time?: number, step?: number, digits?: number): string;
13
+ /**
14
+ * Run the full credentialed login (email → password → TOTP) and finish by
15
+ * minting a JWT via silent OAuth. Mutates `session` in place. Caller persists.
16
+ *
17
+ * Throws with a descriptive message if any step fails (bad password, server-
18
+ * imposed rate limit, Cloudflare challenge, etc.).
19
+ */
20
+ export declare function fullLogin(session: Session, creds: Credentials): Promise<string>;
@@ -0,0 +1,138 @@
1
+ import crypto from "node:crypto";
2
+ import { ingestSetCookie } from "./cookies.js";
3
+ import { client, uniformHeaders, silentOAuth } from "./oauth.js";
4
+ const LOGIN = "https://login.migros.ch";
5
+ /**
6
+ * RFC 6238 TOTP code from a base32-encoded shared secret.
7
+ * Defaults match Google Authenticator / 1Password / Authy: SHA-1, 30s, 6 digits.
8
+ */
9
+ export function totpCode(secretBase32, time = Math.floor(Date.now() / 1000), step = 30, digits = 6) {
10
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
11
+ const cleaned = secretBase32.replace(/=+$/g, "").replace(/\s+/g, "").toUpperCase();
12
+ let bits = "";
13
+ for (const c of cleaned) {
14
+ const v = alphabet.indexOf(c);
15
+ if (v < 0)
16
+ throw new Error(`invalid base32 char in TOTP secret: ${c}`);
17
+ bits += v.toString(2).padStart(5, "0");
18
+ }
19
+ const key = Buffer.alloc(Math.floor(bits.length / 8));
20
+ for (let i = 0; i < key.length; i++) {
21
+ key[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
22
+ }
23
+ const counter = Buffer.alloc(8);
24
+ counter.writeBigUInt64BE(BigInt(Math.floor(time / step)));
25
+ const hmac = crypto.createHmac("sha1", key).update(counter).digest();
26
+ const offset = hmac[hmac.length - 1] & 0x0f;
27
+ const slice = hmac.readUInt32BE(offset) & 0x7fffffff;
28
+ return String(slice % 10 ** digits).padStart(digits, "0");
29
+ }
30
+ function findCsrf(html) {
31
+ const m = html.match(/name="_csrf"[^>]*value="([^"]+)"/);
32
+ if (!m)
33
+ throw new Error("could not find CSRF token in form");
34
+ return m[1];
35
+ }
36
+ function findError(html) {
37
+ // The Migros login form renders errors in a div with class "info-message-error".
38
+ const m = html.match(/info-message-error[^>]*>[\s\S]*?<div[^>]*>([\s\S]{0,200})</);
39
+ return m ? m[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim() : null;
40
+ }
41
+ async function get(url, session, extra = {}) {
42
+ const host = new URL(url).host;
43
+ const r = await client().get(url, { headers: uniformHeaders(host, session, { Accept: "text/html", ...extra }) });
44
+ ingestSetCookie(session, host, r.headers["set-cookie"]);
45
+ return r;
46
+ }
47
+ async function postForm(url, session, body) {
48
+ const host = new URL(url).host;
49
+ const formBody = Object.entries(body)
50
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
51
+ .join("&");
52
+ const r = await client().post(url, formBody, {
53
+ headers: uniformHeaders(host, session, {
54
+ Accept: "text/html",
55
+ "Content-Type": "application/x-www-form-urlencoded",
56
+ Origin: `https://${host}`,
57
+ Referer: url,
58
+ }),
59
+ });
60
+ ingestSetCookie(session, host, r.headers["set-cookie"]);
61
+ return r;
62
+ }
63
+ function locationOf(r, base) {
64
+ const loc = r.headers.location;
65
+ return loc ? new URL(loc, base).toString() : null;
66
+ }
67
+ /**
68
+ * Run the full credentialed login (email → password → TOTP) and finish by
69
+ * minting a JWT via silent OAuth. Mutates `session` in place. Caller persists.
70
+ *
71
+ * Throws with a descriptive message if any step fails (bad password, server-
72
+ * imposed rate limit, Cloudflare challenge, etc.).
73
+ */
74
+ export async function fullLogin(session, creds) {
75
+ // 1. Land on /login/email and capture initial cookies + CSRF.
76
+ let r = await get(`${LOGIN}/login`, session);
77
+ if (r.status >= 300 && r.status < 400) {
78
+ const next = locationOf(r, LOGIN);
79
+ if (next)
80
+ r = await get(next, session);
81
+ }
82
+ let csrf = findCsrf(r.data);
83
+ // 2. POST email.
84
+ r = await postForm(`${LOGIN}/login/email`, session, {
85
+ _csrf: csrf,
86
+ authenticationPayload: "",
87
+ email: creds.email,
88
+ });
89
+ let nextUrl = locationOf(r, LOGIN);
90
+ if (!nextUrl) {
91
+ throw new Error(`email step failed: ${findError(r.data) ?? `status ${r.status}`}`);
92
+ }
93
+ // 3. Server may steer to /login/passkey if the account has one. Force password.
94
+ if (nextUrl.includes("/login/passkey"))
95
+ nextUrl = `${LOGIN}/login/password`;
96
+ // 4. GET password page → capture new CSRF + form action.
97
+ r = await get(nextUrl, session);
98
+ csrf = findCsrf(r.data);
99
+ const passwordAction = r.data.match(/<form[^>]+action="([^"]*)"/)?.[1] || nextUrl;
100
+ const passwordUrl = new URL(passwordAction, nextUrl).toString();
101
+ // 5. POST password.
102
+ r = await postForm(passwordUrl, session, { _csrf: csrf, password: creds.password });
103
+ nextUrl = locationOf(r, LOGIN);
104
+ if (!nextUrl) {
105
+ throw new Error(`password step failed: ${findError(r.data) ?? `status ${r.status}`}`);
106
+ }
107
+ // 6. Server may steer to /login/passkey for the second factor too. Force authenticator.
108
+ if (nextUrl.includes("/login/passkey"))
109
+ nextUrl = `${LOGIN}/login/authenticator`;
110
+ // 7. If a 2FA step is in the chain, handle it. Otherwise skip.
111
+ const needsOtp = /\/login\/(authenticator|second|totp|otp)/.test(nextUrl);
112
+ if (needsOtp) {
113
+ if (!creds.totpSecret)
114
+ throw new Error("server requires 2FA but no TOTP secret was provided");
115
+ r = await get(nextUrl, session);
116
+ csrf = findCsrf(r.data);
117
+ const otpAction = r.data.match(/<form[^>]+action="([^"]*)"/)?.[1] || nextUrl;
118
+ const otpUrl = new URL(otpAction, nextUrl).toString();
119
+ r = await postForm(otpUrl, session, { _csrf: csrf, code: totpCode(creds.totpSecret) });
120
+ nextUrl = locationOf(r, LOGIN);
121
+ if (!nextUrl) {
122
+ throw new Error(`OTP step failed: ${findError(r.data) ?? `status ${r.status}`}`);
123
+ }
124
+ }
125
+ // 8. Follow the post-login redirect chain to settle SSO state.
126
+ let hops = 0;
127
+ while (nextUrl && hops < 10) {
128
+ r = await get(nextUrl, session, { Accept: "text/html,application/json" });
129
+ const loc = locationOf(r, nextUrl);
130
+ if (!loc)
131
+ break;
132
+ nextUrl = loc;
133
+ hops++;
134
+ }
135
+ // 9. Mint the JWT via silent OAuth using the cookies we just collected.
136
+ return silentOAuth(session);
137
+ }
138
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"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"}