albert-heijn-mcp 1.6.0 → 1.7.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/README.md CHANGED
@@ -147,20 +147,27 @@ To install it permanently instead, run `npm install --global albert-heijn-mcp` a
147
147
 
148
148
  ## Logging in
149
149
 
150
- AH's login page has a captcha that only works on AH's own website, so logging in takes two steps. You only do this once.
150
+ Ask your assistant to log you in to Albert Heijn. You only do this once; the login is then kept and renewed automatically.
151
151
 
152
- 1. **Ask your assistant to log you in.** It gives you a link to AH's login page, and on your own computer it also opens it in your browser. Log in as usual.
153
- 2. **Copy the code back into the chat.** After you log in, AH tries to open its phone app, which your browser can't do, so the page seems stuck. The code you need is in the browser's developer console, a panel for web developers that you can open safely:
154
- - Open it in Chrome with <kbd>⌘</kbd> <kbd>⌥</kbd> <kbd>J</kbd> on Mac, or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>J</kbd> on Windows and Linux.
155
- - Find this red line:
152
+ **On your computer** (Claude Desktop, Cursor, VS Code and other local apps), a separate browser window opens with AH's login page. Log in there as usual: the window closes by itself and you're logged in. Then tell your assistant you're done.
156
153
 
157
- ```
158
- Failed to launch 'appie://login-exit?code=…' because the scheme does not have a registered handler.
159
- ```
154
+ The window uses a fresh, empty browser profile, so type your password: saved passwords and password-manager extensions aren't available there. It needs Chrome, Edge, Brave or Chromium (not the snap version on Ubuntu) and a screen; otherwise you copy the code by hand as below.
160
155
 
161
- - Copy the part from `appie://` up to the closing quote and paste it into the chat. The code works once and expires quickly, so paste it right away.
156
+ **On a server** (ChatGPT, Claude.ai), or when no login window can open, you copy a code by hand. AH's login page has a captcha that only works on AH's own website, so the code has to come from your browser:
162
157
 
163
- No line there? See [Troubleshooting](#troubleshooting).
158
+ You need a computer for this, not a phone or tablet: their browsers have no developer console. Chrome, Edge or Brave work best.
159
+
160
+ 1. **Open the link** your assistant gives you, but don't log in yet.
161
+ 2. **Open the developer console** in that tab, a panel for web developers that you can open safely: <kbd>⌘</kbd> <kbd>⌥</kbd> <kbd>J</kbd> on Mac, or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>J</kbd> on Windows and Linux.
162
+ 3. **Log in** as usual. AH then tries to open its phone app, which your browser can't do, so the page seems stuck. In the console, find this red line:
163
+
164
+ ```
165
+ Failed to launch 'appie://login-exit?code=…' because the scheme does not have a registered handler.
166
+ ```
167
+
168
+ 4. **Copy the part from `appie://` up to the closing quote** and paste it into the chat. The code works once and expires quickly, so paste it right away.
169
+
170
+ No line there? See [Troubleshooting](#troubleshooting). To log a server in without the developer console, see [Running it on a server](#running-it-on-a-server).
164
171
 
165
172
  <details>
166
173
  <summary><b>Where your login is stored</b></summary>
@@ -178,10 +185,16 @@ Override the location with `AH_TOKENS_PATH`.
178
185
 
179
186
  ## Troubleshooting
180
187
 
188
+ <details>
189
+ <summary><b>No login window opens</b></summary>
190
+
191
+ The login window needs Chrome, Edge, Brave or Chromium installed in the usual place, and a screen: not over SSH, and not with Ubuntu's snap Chromium. Without one, your assistant gives you the link instead, and you [copy the code by hand](#logging-in). If the window closed before you finished, ask to log in again.
192
+ </details>
193
+
181
194
  <details>
182
195
  <summary><b>No "Failed to launch" line after logging in</b></summary>
183
196
 
184
- Open the developer console before you submit the login form, or look for the `appie://login-exit?code=…` request in the Network tab. Browsers other than Chrome may show the link in an error page or dialog instead.
197
+ The developer console has to be open before you log in. Open it, then open the login link again in that same tab and log in. If AH skips the login form because you're still logged in, that's fine: the line appears all the same. You can also look for the `appie://login-exit?code=…` request in the Network tab. Browsers other than Chrome, Edge or Brave may show the link in an error page or dialog instead.
185
198
  </details>
186
199
 
187
200
  <details>
@@ -196,6 +209,14 @@ Codes work once and expire quickly. Ask to log in again and paste the new link s
196
209
  Ask your assistant to log you out and back in, or delete `tokens.json` from the [login location](#logging-in) and log in again.
197
210
  </details>
198
211
 
212
+ <details>
213
+ <summary><b>"The Albert Heijn login has expired"</b></summary>
214
+
215
+ Albert Heijn no longer accepts the stored login. Ask your assistant to log in to Albert Heijn again. Reconnecting your app to the server doesn't help: that's a separate login.
216
+
217
+ If it keeps expiring on your server, keep `tokens.json` on storage that survives restarts and redeploys, and run a single instance. AH replaces the login every time it's renewed, so an older copy of the file, or a second instance renewing on its own, gets rejected.
218
+ </details>
219
+
199
220
  <details>
200
221
  <summary><b>"Already connected as …" when pasting a login code</b></summary>
201
222
 
@@ -285,15 +306,25 @@ It runs as a hardened systemd service behind a reverse proxy, installed from npm
285
306
  }
286
307
  ```
287
308
 
309
+ To log the server in to Albert Heijn, ask a connected app to log you in and [copy the code by hand](#logging-in). Or skip the code: log in on your own computer with the login window, then copy the login to the server:
310
+
311
+ ```bash
312
+ AH_TOKENS_PATH=./server-tokens.json npx -y albert-heijn-mcp login
313
+ scp server-tokens.json your-server:ah-tokens.json && rm server-tokens.json
314
+ ssh -t your-server 'sudo sh -c "install -d -o albert-heijn-mcp -g albert-heijn-mcp -m 700 /home/albert-heijn-mcp/.config/albert-heijn-mcp && install -o albert-heijn-mcp -g albert-heijn-mcp -m 600 $HOME/ah-tokens.json /home/albert-heijn-mcp/.config/albert-heijn-mcp/tokens.json.new && mv /home/albert-heijn-mcp/.config/albert-heijn-mcp/tokens.json.new /home/albert-heijn-mcp/.config/albert-heijn-mcp/tokens.json && rm $HOME/ah-tokens.json"'
315
+ ```
316
+
317
+ The server picks it up on the next request; no restart needed. The separate file keeps your computer's own login apart: AH replaces a login every time it's renewed, so only one place can use each login. On a host without SSH, upload it to where the server keeps its login: `AH_TOKENS_PATH`, or the [default location](#logging-in) for the user it runs as.
318
+
288
319
  The service can write only to `/home/albert-heijn-mcp`, where it keeps its tokens. If you point `AH_LOG_FILE` elsewhere, add that path to `ReadWritePaths` in the unit file.
289
320
 
290
321
  ## Configuration
291
322
 
292
- Settings are environment variables. They can also go in a `.env` file in the working directory (see [`.env.example`](.env.example)); variables already set in the environment take precedence.
323
+ Settings are environment variables. They can also go in a `.env` file in the working directory (see [`.env.example`](.env.example)); variables already set in the environment take precedence. On a hosting platform, set them in its environment variable or secret settings instead of a `.env` file, so `AH_MCP_TOKEN` stays out of the app's files.
293
324
 
294
325
  | Variable | Default | Description |
295
326
  |---|---|---|
296
- | `AH_REMOTE` | `false` | Don't open a browser on login (same as `--remote`). Always on with `streamable-http`. |
327
+ | `AH_REMOTE` | `false` | Don't open a browser on login; log in by copying the code instead (same as `--remote`). Always on with `streamable-http`. |
297
328
  | `AH_TOKENS_PATH` | [per OS](#logging-in) | Where to store login tokens. |
298
329
  | `AH_MCP_HOST` | `127.0.0.1` | Interface the HTTP server listens on. Keep the default behind a reverse proxy. |
299
330
  | `AH_MCP_PORT` | `3000` | HTTP server port. |
@@ -305,9 +336,10 @@ Command-line flags:
305
336
 
306
337
  ```
307
338
  node dist/index.js [--transport stdio|streamable-http] [--remote] [--version] [--help]
339
+ node dist/index.js login [--remote]
308
340
  ```
309
341
 
310
- `stdio` (the default) is for local clients; `streamable-http` serves MCP at `/mcp`, with OAuth login at `/authorize`.
342
+ `stdio` (the default) is for local clients; `streamable-http` serves MCP at `/mcp`, with OAuth login at `/authorize`. `login` logs in to Albert Heijn from the terminal, saves the tokens and exits; with `--remote` it asks for the code instead of opening a login window.
311
343
 
312
344
  ## Tools
313
345
 
@@ -320,7 +352,7 @@ Tools that return data also return it as [structured output](https://modelcontex
320
352
 
321
353
  | Tool | Description |
322
354
  |---|---|
323
- | `ah_login` | Log in: returns AH's login link, then completes the login with the code you paste back. |
355
+ | `ah_login` | Log in: opens a login window on your computer, or returns AH's login link and completes the login with the code you paste back. |
324
356
  | `ah_logout` | Delete the stored tokens, to switch accounts or reset a session. |
325
357
  | `ah_get_member_profile` | Name, masked email, and bonus card number (last 4 digits). |
326
358
 
@@ -419,9 +451,9 @@ Run it from the checkout with `node dist/index.js`, or use `/path/to/albert-heij
419
451
 
420
452
  | Path | Contents |
421
453
  |---|---|
422
- | [`src/index.ts`](src/index.ts), [`src/config.ts`](src/config.ts) | Entry point, flags and settings |
454
+ | [`src/index.ts`](src/index.ts), [`src/config.ts`](src/config.ts), [`src/loginCommand.ts`](src/loginCommand.ts) | Entry point, flags and settings, and the `login` command |
423
455
  | [`src/ahapi/`](src/ahapi) | Client for AH's REST and GraphQL API, on Node's built-in `fetch` |
424
- | [`src/auth/`](src/auth) | Login code exchange, token storage and refresh |
456
+ | [`src/auth/`](src/auth) | Login window, login code exchange, token storage and refresh |
425
457
  | [`src/server/`](src/server) | Streamable HTTP transport and OAuth login |
426
458
  | [`src/tools/`](src/tools) | The MCP tools, one file per area |
427
459
  | [`deploy/`](deploy) | systemd unit, shipped in the package |
@@ -35,6 +35,14 @@ export class AhApiError extends Error {
35
35
  export function hasStatus(err, status) {
36
36
  return err instanceof AhApiError && err.status === status;
37
37
  }
38
+ /** Reports whether err, or any error in its cause chain, is an HTTP 401 from AH. */
39
+ export function isUnauthorized(err) {
40
+ for (let e = err; e instanceof Error; e = e.cause) {
41
+ if (hasStatus(e, 401))
42
+ return true;
43
+ }
44
+ return false;
45
+ }
38
46
  /** An error with our own context in front of its cause's message. */
39
47
  export class ContextError extends Error {
40
48
  /** Our text, without the cause's (which may quote AH). */
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ahapi/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ahapi/client.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,7 @@
1
+ /** "First Last", as AH shows it. */
2
+ export function fullName(m) {
3
+ return `${m.firstName} ${m.lastName}`.trim();
4
+ }
1
5
  export async function getMember(c) {
2
6
  const query = `query Member {
3
7
  member {
@@ -1 +1 @@
1
- {"version":3,"file":"member.js","sourceRoot":"","sources":["../../src/ahapi/member.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,CAAW;IACzC,MAAM,KAAK,GAAG;;;;;;;EAOd,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,OAAO,CAOlC,KAAK,CAAC,CAAC;IACV,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;QAC9B,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QAC5B,KAAK,EAAE,CAAC,CAAC,YAAY,IAAI,EAAE;QAC3B,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,IAAI,EAAE;QACvC,eAAe,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QACrC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAA4C;IAC9D,OAAO,OAAO,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC;AACxF,CAAC"}
1
+ {"version":3,"file":"member.js","sourceRoot":"","sources":["../../src/ahapi/member.ts"],"names":[],"mappings":"AAsBA,oCAAoC;AACpC,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,OAAO,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,CAAW;IACzC,MAAM,KAAK,GAAG;;;;;;;EAOd,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,OAAO,CAOlC,KAAK,CAAC,CAAC;IACV,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;QAC9B,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QAC5B,KAAK,EAAE,CAAC,CAAC,YAAY,IAAI,EAAE;QAC3B,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,IAAI,EAAE;QACvC,eAAe,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QACrC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAA4C;IAC9D,OAAO,OAAO,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,347 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { delimiter, join } from "node:path";
6
+ import { log } from "../log.js";
7
+ import { findCode } from "./login.js";
8
+ // Logging in without copying the code by hand: AH's login page ends by redirecting to the AH app's
9
+ // appie://login-exit?code=... link, which a browser can't open. A Chromium-based browser started with
10
+ // the DevTools protocol reports that redirect, so the code can be read from it.
11
+ /** How long the login window waits for the user. */
12
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
13
+ /** How long the browser gets to close before it is killed. */
14
+ const CLOSE_TIMEOUT_MS = 5000;
15
+ /** Temporary browser profiles are directories in the temp dir starting with this. */
16
+ const PROFILE_PREFIX = "albert-heijn-mcp-login-";
17
+ /** How long a new login window gets to fail before it counts as open. */
18
+ const START_CHECK_MS = 1500;
19
+ /** How long a failed login window is still worth mentioning. */
20
+ const ERROR_TTL_MS = 15 * 60_000;
21
+ /** Path of an installed Chromium-based browser (Chrome, Edge, Brave or Chromium) that can show a window. */
22
+ export function findBrowser() {
23
+ return browserCandidates().find((path) => existsSync(path));
24
+ }
25
+ function browserCandidates() {
26
+ switch (process.platform) {
27
+ case "darwin": {
28
+ // Over SSH there is no screen to show a window on.
29
+ if (process.env.SSH_CONNECTION)
30
+ return [];
31
+ const apps = ["Google Chrome", "Microsoft Edge", "Brave Browser", "Chromium"];
32
+ const dirs = ["/Applications", join(homedir(), "Applications")];
33
+ return apps.flatMap((app) => dirs.map((dir) => join(dir, `${app}.app`, "Contents", "MacOS", app)));
34
+ }
35
+ case "win32": {
36
+ if (process.env.SSH_CONNECTION)
37
+ return [];
38
+ const exes = [
39
+ ["Google", "Chrome", "Application", "chrome.exe"],
40
+ ["Microsoft", "Edge", "Application", "msedge.exe"],
41
+ ["BraveSoftware", "Brave-Browser", "Application", "brave.exe"],
42
+ ["Chromium", "Application", "chrome.exe"],
43
+ ];
44
+ const roots = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
45
+ return exes.flatMap((exe) => roots.filter((root) => root !== undefined).map((root) => join(root, ...exe)));
46
+ }
47
+ default: {
48
+ // Without a display (a server, an SSH session) a window can't be shown.
49
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
50
+ return [];
51
+ // Snap browsers (Ubuntu's Chromium, also behind /usr/bin/chromium-browser) have a private /tmp,
52
+ // so their profile would land where it can't be removed.
53
+ const snapChromium = existsSync("/snap/bin/chromium");
54
+ const names = ["google-chrome", "google-chrome-stable", "microsoft-edge", "brave-browser"];
55
+ if (!snapChromium)
56
+ names.push("chromium", "chromium-browser");
57
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter((dir) => dir && !dir.startsWith("/snap/"));
58
+ return names.flatMap((name) => dirs.map((dir) => join(dir, name)));
59
+ }
60
+ }
61
+ }
62
+ /** Opens url in the default browser; failures are ignored (the link is also returned). */
63
+ export function openInDefaultBrowser(url) {
64
+ const [cmd, args] = process.platform === "darwin"
65
+ ? ["open", [url]]
66
+ : process.platform === "win32"
67
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
68
+ : ["xdg-open", [url]];
69
+ try {
70
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => { }).unref();
71
+ }
72
+ catch {
73
+ // Ignored.
74
+ }
75
+ }
76
+ /** A login in a browser window that finishes in the background, e.g. while a tool call has returned. */
77
+ export class BrowserLogin {
78
+ #window;
79
+ /** Set while the code from the window is being exchanged and saved. */
80
+ #saving;
81
+ #failed;
82
+ /** Whether a login window is open and still waiting for the user. */
83
+ get running() {
84
+ return this.#window !== undefined && this.#saving === undefined;
85
+ }
86
+ /** Why the last login window didn't finish, if that was recent. */
87
+ get error() {
88
+ return this.#failed && Date.now() - this.#failed.at < ERROR_TTL_MS ? this.#failed.error : undefined;
89
+ }
90
+ /** Waits while a code from the window is being saved, so the login is stored once this returns. */
91
+ async saved() {
92
+ await this.#saving?.catch(() => { });
93
+ }
94
+ /** Closes the login window, if one is open, and waits until it has. */
95
+ async cancel() {
96
+ this.#window?.abort.abort();
97
+ await this.#window?.done;
98
+ }
99
+ /**
100
+ * Opens a login window at url; onCode runs with the code once the user has logged in. Returns false
101
+ * if no suitable browser is installed or the window failed right away (see error), e.g. no screen.
102
+ */
103
+ async start(url, onCode) {
104
+ this.#failed = undefined;
105
+ const browser = findBrowser();
106
+ if (!browser)
107
+ return false;
108
+ // One window at a time; a code the old one already has is saved (or reported) first.
109
+ await this.saved();
110
+ this.#window?.abort.abort();
111
+ this.#saving = undefined;
112
+ const abort = new AbortController();
113
+ const fail = (err) => {
114
+ // A cancel is on purpose (e.g. the login was finished another way), not a failure.
115
+ if (abort.signal.aborted)
116
+ return;
117
+ log.warn("browser login failed", { err });
118
+ this.#failed = { error: err instanceof Error ? err : new Error(String(err)), at: Date.now() };
119
+ };
120
+ // Saving failures are reported right away, not only once the window has closed.
121
+ const save = (code) => (this.#saving = onCode(code).catch(fail));
122
+ const window = {
123
+ abort,
124
+ done: loginInWindow(url, browser, save, abort.signal)
125
+ .catch(fail)
126
+ .finally(() => {
127
+ // Only this window's state: a newer window may have started while this one closed.
128
+ if (this.#window !== window)
129
+ return;
130
+ this.#window = undefined;
131
+ this.#saving = undefined;
132
+ }),
133
+ };
134
+ this.#window = window;
135
+ // A browser that can't show a window usually exits at once.
136
+ let timer;
137
+ await Promise.race([window.done, new Promise((resolve) => (timer = setTimeout(resolve, START_CHECK_MS)))]);
138
+ clearTimeout(timer);
139
+ return this.#window === window && this.#failed === undefined;
140
+ }
141
+ }
142
+ /**
143
+ * Opens url in a new window of browser, with a fresh profile; once the page redirects to the appie://
144
+ * link, calls use with the login code and resolves with its result. The window closes afterwards, so
145
+ * the short-lived code is used first. Rejects if the window is closed, times out or signal aborts first.
146
+ * The browser is controlled over a pipe, not a network port, so no other program can attach to it.
147
+ */
148
+ export async function loginInWindow(url, browser, use, signal) {
149
+ await removeOldProfiles();
150
+ signal?.throwIfAborted();
151
+ const profile = await mkdtemp(join(tmpdir(), PROFILE_PREFIX));
152
+ if (signal?.aborted) {
153
+ await rm(profile, { recursive: true, force: true });
154
+ signal.throwIfAborted();
155
+ }
156
+ let child;
157
+ try {
158
+ child = spawn(browser, [`--user-data-dir=${profile}`, "--remote-debugging-pipe", "--no-first-run", "--no-default-browser-check", url],
159
+ // Its own process group, so Ctrl+C in a terminal reaches only this process, which then closes it.
160
+ { stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
161
+ }
162
+ catch (err) {
163
+ await rm(profile, { recursive: true, force: true });
164
+ throw err;
165
+ }
166
+ // "exit", not "close": a helper process may keep the pipes open after the browser has gone.
167
+ const exited = new Promise((resolve) => child.once("exit", () => resolve()));
168
+ const toBrowser = child.stdio[3];
169
+ const fromBrowser = child.stdio[4];
170
+ // No pipes if it couldn't start at all (e.g. too many open files).
171
+ if (!toBrowser || !fromBrowser) {
172
+ child.kill("SIGKILL");
173
+ await rm(profile, { recursive: true, force: true });
174
+ throw new Error("the browser could not be started");
175
+ }
176
+ // Errors also end the browser process, which ends the login.
177
+ toBrowser.on("error", () => { });
178
+ fromBrowser.on("error", () => { });
179
+ let lastId = 0;
180
+ /** Sends a command; returns its id. */
181
+ const send = (method, params = {}, sessionId) => {
182
+ toBrowser.write(`${JSON.stringify({ id: ++lastId, method, params, sessionId })}\0`);
183
+ return lastId;
184
+ };
185
+ // The code counts only when AH's login site sends the tab there: a navigation of a tab's main frame
186
+ // while it shows that site, or a redirect from it. Any other page could plant someone else's code.
187
+ const mainFrames = new Map();
188
+ // On macOS the browser keeps running after its last window closes, so watch the tabs instead.
189
+ const pages = new Set();
190
+ let onAllClosed = () => { };
191
+ const frameTreeRequests = new Map();
192
+ const onMessage = (raw) => {
193
+ // Most messages are network events; parse only the few that can matter.
194
+ if (!/appie:\/\/login-exit\?|"method":"(Target\.|Page\.frameNavigated)|"frameTree"/.test(raw))
195
+ return undefined;
196
+ const msg = JSON.parse(raw);
197
+ const p = msg.params ?? {};
198
+ const sessionId = msg.sessionId ?? (msg.id !== undefined ? frameTreeRequests.get(msg.id) : undefined);
199
+ const frame = p.frame ?? msg.result?.frameTree?.frame;
200
+ if (frame && !frame.parentId && sessionId)
201
+ mainFrames.set(sessionId, { id: frame.id, url: frame.url });
202
+ switch (msg.method) {
203
+ case "Target.targetCreated":
204
+ if (p.targetInfo?.type === "page") {
205
+ pages.add(p.targetInfo.targetId);
206
+ send("Target.attachToTarget", { targetId: p.targetInfo.targetId, flatten: true });
207
+ }
208
+ break;
209
+ case "Target.targetDestroyed":
210
+ if (p.targetId && pages.delete(p.targetId) && pages.size === 0)
211
+ onAllClosed();
212
+ break;
213
+ case "Target.attachedToTarget":
214
+ if (p.sessionId) {
215
+ send("Page.enable", {}, p.sessionId);
216
+ // The page may have loaded before Page.enable, so ask which one it shows.
217
+ frameTreeRequests.set(send("Page.getFrameTree", {}, p.sessionId), p.sessionId);
218
+ // No response bodies are needed, so Chrome needn't keep them.
219
+ send("Network.enable", { maxTotalBufferSize: 0, maxResourceBufferSize: 0 }, p.sessionId);
220
+ }
221
+ break;
222
+ case "Page.frameRequestedNavigation":
223
+ case "Page.frameScheduledNavigation": {
224
+ const main = msg.sessionId ? mainFrames.get(msg.sessionId) : undefined;
225
+ if (main && p.frameId === main.id && isAhPage(main.url))
226
+ return redirectCode(p.url);
227
+ break;
228
+ }
229
+ case "Network.requestWillBeSent": {
230
+ // Only the tab's own page: an embedded frame or image could be sent there through an ah.nl redirect.
231
+ const main = msg.sessionId ? mainFrames.get(msg.sessionId) : undefined;
232
+ if (p.type === "Document" && main && p.frameId === main.id && p.redirectResponse && isAhPage(p.redirectResponse.url)) {
233
+ return redirectCode(p.request?.url);
234
+ }
235
+ break;
236
+ }
237
+ }
238
+ return undefined;
239
+ };
240
+ let timer;
241
+ try {
242
+ const code = await new Promise((resolve, reject) => {
243
+ timer = setTimeout(() => reject(new Error("the login window timed out")), LOGIN_TIMEOUT_MS);
244
+ const closed = () => reject(new Error("the login window was closed before the login finished"));
245
+ void exited.then(closed);
246
+ onAllClosed = closed;
247
+ const cancelled = () => reject(new Error("the login was cancelled"));
248
+ if (signal?.aborted)
249
+ cancelled();
250
+ signal?.addEventListener("abort", cancelled, { once: true });
251
+ child.once("error", reject);
252
+ readMessages(fromBrowser, (raw) => {
253
+ try {
254
+ const code = onMessage(raw);
255
+ if (code)
256
+ resolve(code);
257
+ }
258
+ catch {
259
+ // Not a message we can use.
260
+ }
261
+ });
262
+ // Also reports the tabs that are already open.
263
+ send("Target.setDiscoverTargets", { discover: true });
264
+ });
265
+ return await use(code);
266
+ }
267
+ finally {
268
+ clearTimeout(timer);
269
+ fromBrowser.removeAllListeners("data");
270
+ // Not if it failed to start or has already exited.
271
+ if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
272
+ send("Browser.close");
273
+ const killTimer = setTimeout(() => killBrowser(child), CLOSE_TIMEOUT_MS);
274
+ await exited;
275
+ clearTimeout(killTimer);
276
+ }
277
+ // A helper process may still hold the pipes; they'd keep this process running.
278
+ toBrowser.destroy();
279
+ fromBrowser.destroy();
280
+ await rm(profile, { recursive: true, force: true, maxRetries: 5 }).catch(() => { });
281
+ }
282
+ }
283
+ /** Kills the browser and, where it has its own process group, its helper processes too. */
284
+ function killBrowser(child) {
285
+ try {
286
+ if (process.platform !== "win32" && child.pid !== undefined)
287
+ process.kill(-child.pid, "SIGKILL");
288
+ else
289
+ child.kill("SIGKILL");
290
+ }
291
+ catch {
292
+ child.kill("SIGKILL");
293
+ }
294
+ }
295
+ /**
296
+ * Removes temporary profiles left behind by a process that was killed during a login: they may hold
297
+ * AH cookies. Only ones older than any login window can be, so a window open elsewhere is left alone.
298
+ */
299
+ async function removeOldProfiles() {
300
+ // Twice the timeout: saving the code can still take a while after the window got it.
301
+ const cutoff = Date.now() - 2 * LOGIN_TIMEOUT_MS;
302
+ let names;
303
+ try {
304
+ names = await readdir(tmpdir());
305
+ }
306
+ catch {
307
+ return;
308
+ }
309
+ for (const name of names.filter((n) => n.startsWith(PROFILE_PREFIX))) {
310
+ const path = join(tmpdir(), name);
311
+ try {
312
+ if ((await stat(path)).mtimeMs < cutoff)
313
+ await rm(path, { recursive: true, force: true });
314
+ }
315
+ catch {
316
+ // E.g. another user's, or already gone; the others still get removed.
317
+ }
318
+ }
319
+ }
320
+ /** Calls onMessage with each message from the browser: JSON text, each ending in a NUL byte. */
321
+ function readMessages(stream, onMessage) {
322
+ let buffered = "";
323
+ stream.setEncoding("utf8");
324
+ stream.on("data", (chunk) => {
325
+ buffered += chunk;
326
+ let start = 0;
327
+ for (let end = buffered.indexOf("\0"); end >= 0; end = buffered.indexOf("\0", start)) {
328
+ onMessage(buffered.slice(start, end));
329
+ start = end + 1;
330
+ }
331
+ buffered = buffered.slice(start);
332
+ });
333
+ }
334
+ /** The login code if url is the appie://login-exit link itself, not merely mentions it. */
335
+ function redirectCode(url) {
336
+ return url?.startsWith("appie://login-exit?") ? findCode(url) : undefined;
337
+ }
338
+ /** Reports whether url is a page of AH's login site, which sends the browser to the appie:// link. */
339
+ function isAhPage(url) {
340
+ try {
341
+ return new URL(url).hostname === "login.ah.nl";
342
+ }
343
+ catch {
344
+ return false;
345
+ }
346
+ }
347
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/auth/browser.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,33 @@
1
+ import { LOGIN_URL } from "../ahapi/index.js";
1
2
  /** code=... in the pasted appie://login-exit link or the console line quoting it; links elsewhere don't count. */
2
3
  const CODE_PARAM = /appie:\/\/login-exit\?(?:[^\s'"]*&)?code=([A-Za-z0-9._~-]+)/;
3
4
  /** A bare code. */
4
5
  const BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
6
+ /** The login code in an appie://login-exit link anywhere in text, if there is one. */
7
+ export function findCode(text) {
8
+ return CODE_PARAM.exec(text)?.[1];
9
+ }
5
10
  /** Extracts the login code from the pasted redirect link, console line, or bare code. */
6
11
  export function extractCode(input) {
7
12
  const trimmed = input.trim();
8
- const match = CODE_PARAM.exec(trimmed);
9
- if (match?.[1])
10
- return match[1];
13
+ const code = findCode(trimmed);
14
+ if (code)
15
+ return code;
11
16
  if (BARE_CODE.test(trimmed))
12
17
  return trimmed;
13
18
  throw new Error("no login code found: paste the appie://login-exit?code=... link or the code itself");
14
19
  }
20
+ /** Said before a login when AH no longer accepts the stored one. */
21
+ export const STALE_LOGIN = "The stored Albert Heijn login no longer works, so log in to Albert Heijn again.";
22
+ /** The steps for logging in by copying the code by hand; paste says where the last step pastes the link. */
23
+ export function manualLoginSteps(opened, paste) {
24
+ return `1. Open this link in Chrome, Edge or Brave${opened ? " (it may have opened in your default browser; use that tab only if it's one of these)" : ""}, but don't log in yet:
25
+ ${LOGIN_URL}
26
+ 2. On that page, open the developer console first: Cmd+Option+J on Mac, Ctrl+Shift+J on Windows/Linux.
27
+ It's a panel for web developers and safe to open. If you already logged in, open the link again in that same tab.
28
+ 3. Log in. The page then seems stuck: AH tries to open its phone app, which the browser can't.
29
+ In the console, find the red line "Failed to launch 'appie://login-exit?code=...'" and copy
30
+ the link from appie:// up to the closing quote. Other browsers may show the link in an error page instead.
31
+ 4. Paste that link ${paste}. It works once and expires quickly, so do this right away.`;
32
+ }
15
33
  //# sourceMappingURL=login.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,MAAM,UAAU,GAAG,6DAA6D,CAAC;AACjF,mBAAmB;AACnB,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAE1C,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC5C,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;AACxG,CAAC"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,kHAAkH;AAClH,MAAM,UAAU,GAAG,6DAA6D,CAAC;AACjF,mBAAmB;AACnB,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAE1C,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC5C,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;AACxG,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,iFAAiF,CAAC;AAE7G,4GAA4G;AAC5G,MAAM,UAAU,gBAAgB,CAAC,MAAe,EAAE,KAAa;IAC7D,OAAO,6CAA6C,MAAM,CAAC,CAAC,CAAC,uFAAuF,CAAC,CAAC,CAAC,EAAE;KACtJ,SAAS;;;;;;qBAMO,KAAK,6DAA6D,CAAC;AACxF,CAAC"}