continentept-mcp 0.0.0-stage → 0.2.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 (85) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +33 -0
  3. package/LICENSE +21 -0
  4. package/README.md +156 -2
  5. package/dist/application.d.ts +12 -0
  6. package/dist/application.js +5 -0
  7. package/dist/auto-login.d.ts +3 -0
  8. package/dist/auto-login.js +10 -0
  9. package/dist/cli.d.ts +6 -0
  10. package/dist/cli.js +101 -0
  11. package/dist/clients/authentication.d.ts +26 -0
  12. package/dist/clients/authentication.js +92 -0
  13. package/dist/clients/policy.d.ts +26 -0
  14. package/dist/clients/policy.js +37 -0
  15. package/dist/clients/storefront.d.ts +40 -0
  16. package/dist/clients/storefront.js +319 -0
  17. package/dist/config.d.ts +25 -0
  18. package/dist/config.js +74 -0
  19. package/dist/domain/errors.d.ts +46 -0
  20. package/dist/domain/errors.js +17 -0
  21. package/dist/domain/identity.d.ts +22 -0
  22. package/dist/domain/identity.js +40 -0
  23. package/dist/domain/index.d.ts +9 -0
  24. package/dist/domain/index.js +9 -0
  25. package/dist/domain/models.d.ts +345 -0
  26. package/dist/domain/models.js +67 -0
  27. package/dist/domain/money.d.ts +23 -0
  28. package/dist/domain/money.js +30 -0
  29. package/dist/domain/outcomes.d.ts +147 -0
  30. package/dist/domain/outcomes.js +38 -0
  31. package/dist/domain/quantity.d.ts +94 -0
  32. package/dist/domain/quantity.js +41 -0
  33. package/dist/domain/result.d.ts +15 -0
  34. package/dist/domain/result.js +7 -0
  35. package/dist/domain/session.d.ts +40 -0
  36. package/dist/domain/session.js +11 -0
  37. package/dist/domain/validation.d.ts +6 -0
  38. package/dist/domain/validation.js +28 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.js +5 -0
  41. package/dist/main-module.d.ts +2 -0
  42. package/dist/main-module.js +7 -0
  43. package/dist/mcp/contracts.d.ts +1474 -0
  44. package/dist/mcp/contracts.js +65 -0
  45. package/dist/mcp/guide.d.ts +5 -0
  46. package/dist/mcp/guide.js +67 -0
  47. package/dist/mcp/server.d.ts +49 -0
  48. package/dist/mcp/server.js +114 -0
  49. package/dist/mcp/shopping.d.ts +33 -0
  50. package/dist/mcp/shopping.js +220 -0
  51. package/dist/parsers/cart.d.ts +48 -0
  52. package/dist/parsers/cart.js +229 -0
  53. package/dist/parsers/catalog.d.ts +37 -0
  54. package/dist/parsers/catalog.js +191 -0
  55. package/dist/parsers/orders.d.ts +19 -0
  56. package/dist/parsers/orders.js +158 -0
  57. package/dist/parsers/product.d.ts +3 -0
  58. package/dist/parsers/product.js +44 -0
  59. package/dist/parsers/records.d.ts +3 -0
  60. package/dist/parsers/records.js +12 -0
  61. package/dist/ports.d.ts +45 -0
  62. package/dist/ports.js +1 -0
  63. package/dist/runtime.d.ts +26 -0
  64. package/dist/runtime.js +40 -0
  65. package/dist/services/cart.d.ts +19 -0
  66. package/dist/services/cart.js +193 -0
  67. package/dist/services/catalog.d.ts +89 -0
  68. package/dist/services/catalog.js +118 -0
  69. package/dist/services/orders.d.ts +42 -0
  70. package/dist/services/orders.js +173 -0
  71. package/dist/services/recovery.d.ts +7 -0
  72. package/dist/services/recovery.js +17 -0
  73. package/dist/session/browser.d.ts +60 -0
  74. package/dist/session/browser.js +177 -0
  75. package/dist/session/queue.d.ts +5 -0
  76. package/dist/session/queue.js +35 -0
  77. package/dist/session/request.d.ts +37 -0
  78. package/dist/session/request.js +82 -0
  79. package/dist/stores/files.d.ts +59 -0
  80. package/dist/stores/files.js +106 -0
  81. package/docs/configuration.md +113 -0
  82. package/docs/tools.md +223 -0
  83. package/docs/troubleshooting.md +25 -0
  84. package/package.json +75 -4
  85. package/skills/basket-planner/SKILL.md +50 -0
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # continentept-mcp settings.
2
+ # Save as ~/.continentept-mcp/credentials.env, or anywhere you point CONTINENTE_CREDENTIALS_FILE at.
3
+ # Keep it private (chmod 600). Values set in the MCP client environment take precedence.
4
+
5
+ # Account credentials for automatic sign-in. Not needed for search or product details.
6
+ CONTINENTE_EMAIL=you@example.com
7
+ CONTINENTE_PASSWORD=replace-with-your-password
8
+
9
+ # Close the browser after this many idle seconds; 0 keeps it open. Default: 300
10
+ # CONTINENTE_BROWSER_IDLE_SECONDS=300
11
+
12
+ # These two only work as environment variables in the MCP client configuration:
13
+ # CONTINENTE_CREDENTIALS_FILE=/private/path/credentials.env
14
+ # CONTINENTE_DATA_DIR=/private/path/continentept-mcp
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-10-15
10
+
11
+ ### Added
12
+
13
+ - Server instructions: assistants receive the cart rules (exact totals, one change at a time, check the cart after an uncertain change) as soon as they connect.
14
+ - A `basket-planner` prompt with the full shopping workflow, with an optional request to plan for. No skill install is needed.
15
+ - Documentation for supplying credentials from 1Password with `op run`.
16
+
17
+ ### Changed
18
+
19
+ - The `basket-planner` skill links to the online tool reference, so it also works when uploaded on its own.
20
+
21
+ ## [0.1.0] - 2026-10-15
22
+
23
+ ### Added
24
+
25
+ - Catalogue search and product details with exact prices, availability and sale rules.
26
+ - Cart tools that confirm every change by reading the cart back and report uncertain changes instead of retrying them.
27
+ - Favorites, order history and a ranking of frequently bought products.
28
+ - Automatic sign-in from environment variables or a private credentials file, and a `login` command.
29
+ - The `basket-planner` skill for planning a shopping list and updating the basket.
30
+
31
+ [Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.2.0...HEAD
32
+ [0.2.0]: https://github.com/andredelgado/continente-mcp/compare/v0.1.0...v0.2.0
33
+ [0.1.0]: https://github.com/andredelgado/continente-mcp/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 André Delgado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # Continente MCP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/andredelgado/continente-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/andredelgado/continente-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/continentept-mcp)](https://www.npmjs.com/package/continentept-mcp)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ An [MCP](https://modelcontextprotocol.io) server that lets AI assistants shop at
8
+ [Continente.pt](https://www.continente.pt): search products, check prices and sale rules,
9
+ read your favorites and past orders, and fill your cart. Checkout and payment always stay
10
+ on the website.
11
+
12
+ > **Unofficial.** This project is not affiliated with, endorsed by or supported by
13
+ > Continente or MC Sonae. It automates the public website with a headless browser, may break
14
+ > when the site changes, and should be used in line with the website's terms. You are
15
+ > responsible for anything placed in your cart.
16
+
17
+ ## Features
18
+
19
+ - **Catalogue search and product details**, with exact prices, unit prices, availability and
20
+ sale rules for weighted and bulk products.
21
+ - **Safe cart changes.** Every change is confirmed by reading the cart back. If a change
22
+ cannot be confirmed, the result says so and tells the assistant to check the cart instead
23
+ of retrying.
24
+ - **Your account**: favorites, order history and a ranking of the products you buy most.
25
+ - **Runs locally** over stdio. Credentials and cookies stay on your machine.
26
+ - **Built-in guidance.** The server tells the assistant the cart rules as soon as it
27
+ connects, and offers a `basket-planner` prompt with the full shopping workflow. Nothing
28
+ extra to install; the same guide also ships as a [skill](skills/basket-planner/SKILL.md).
29
+
30
+ ## Requirements
31
+
32
+ - Node.js 22 or newer
33
+ - A Continente.pt account for account features (search and product details work without one)
34
+
35
+ ## Quick start
36
+
37
+ **1. Install the browser** the server uses to talk to the website (one-time download):
38
+
39
+ ```sh
40
+ npx -y continentept-mcp install-browser
41
+ ```
42
+
43
+ On Linux, append `--with-deps` to also install the required system libraries.
44
+
45
+ **2. Save your credentials** in `~/.continentept-mcp/credentials.env` (see
46
+ [configuration](docs/configuration.md) for other options):
47
+
48
+ ```sh
49
+ mkdir -p ~/.continentept-mcp && chmod 700 ~/.continentept-mcp
50
+ printf 'CONTINENTE_EMAIL=you@example.com\nCONTINENTE_PASSWORD=your-password\n' > ~/.continentept-mcp/credentials.env
51
+ chmod 600 ~/.continentept-mcp/credentials.env
52
+ ```
53
+
54
+ Using 1Password? Skip the file and pass secret references through `op run` instead; see
55
+ [Using 1Password](docs/configuration.md#using-1password).
56
+
57
+ Optionally check that login works:
58
+
59
+ ```sh
60
+ npx -y continentept-mcp login
61
+ ```
62
+
63
+ **3. Add the server to your MCP client.**
64
+
65
+ Claude Code:
66
+
67
+ ```sh
68
+ claude mcp add continente -- npx -y continentept-mcp
69
+ ```
70
+
71
+ Claude Desktop, Cursor and other clients (`claude_desktop_config.json`, `mcp.json`, ...):
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "continente": {
77
+ "command": "npx",
78
+ "args": ["-y", "continentept-mcp"]
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ To keep the credentials file elsewhere, add
85
+ `"env": { "CONTINENTE_CREDENTIALS_FILE": "/absolute/path/to/credentials.env" }`.
86
+
87
+ Then ask your assistant something like *"Add semi-skimmed milk and a dozen eggs to my
88
+ Continente cart"* or *"What do I usually buy? Build me a list for this week."*
89
+
90
+ ## Tools
91
+
92
+ | Tool | What it does |
93
+ | --- | --- |
94
+ | `search_products` | Search the catalogue; favorites rank first |
95
+ | `get_product` | Price, availability and sale rules for one product |
96
+ | `list_favorites` | Your saved favorites (`refresh: true` fetches the latest) |
97
+ | `get_cart` | Cart lines, quantities and total |
98
+ | `add_cart_item` | Add an amount on top of what is in the cart |
99
+ | `set_cart_item_quantity` | Make the cart hold exactly a quantity; zero removes |
100
+ | `remove_cart_item` | Remove a line from the cart |
101
+ | `list_orders` | Recent orders with their product lines |
102
+ | `get_order` | One order's product lines |
103
+ | `list_frequently_bought_products` | Products ranked by how often you order them |
104
+ | `get_session_status` | Local login, cookie and cache state (no network) |
105
+
106
+ See the [tool reference](docs/tools.md) for inputs, outputs, error codes and limits.
107
+
108
+ ### About uncertain cart changes
109
+
110
+ A cart change can fail after the request has reached the website, for example on a timeout.
111
+ In that case the result has `mutation.outcome: "unknown"` and asks the client to call
112
+ `get_cart` before doing anything else. The server never retries such a change on its own,
113
+ and clients should not either: repeating an `add_cart_item` could add the item twice.
114
+
115
+ ### Guidance for the assistant
116
+
117
+ When a client connects, the server sends short instructions with these cart rules, so the
118
+ assistant follows them without any setup. For planning a shopping list, pick the
119
+ **basket-planner** prompt from your client's prompt or attachment menu, optionally with a
120
+ request such as "weekly basket for two".
121
+
122
+ The same guide is available as a skill in
123
+ [`skills/basket-planner`](skills/basket-planner/SKILL.md). Clients that support skills, such
124
+ as Claude Desktop (Settings → Capabilities → Skills) or Claude Code (`~/.claude/skills/`),
125
+ then apply it automatically whenever you ask about groceries.
126
+
127
+ ## Command line
128
+
129
+ ```text
130
+ continentept-mcp Start the MCP server on stdio
131
+ continentept-mcp login Sign in and save the session (--show-browser to watch)
132
+ continentept-mcp install-browser Download Chromium (extra arguments go to Playwright)
133
+ continentept-mcp --version
134
+ ```
135
+
136
+ ## Limitations
137
+
138
+ - One account per server process.
139
+ - Searches read at most five result pages and order listings at most ten pages, so very
140
+ broad queries or long histories are reported as incomplete rather than read in full.
141
+ - Login handles the standard email and password flow. If the website asks for extra
142
+ verification, run `continentept-mcp login --show-browser` and complete it in the browser
143
+ window.
144
+ - There are no checkout, payment, delivery-slot or favorites-editing tools.
145
+
146
+ ## Documentation
147
+
148
+ - [Tool reference](docs/tools.md)
149
+ - [Configuration](docs/configuration.md)
150
+ - [Troubleshooting](docs/troubleshooting.md)
151
+ - [Contributing](CONTRIBUTING.md)
152
+ - [Security policy](SECURITY.md)
153
+ - [Changelog](CHANGELOG.md)
154
+
155
+ ## License
156
+
157
+ [MIT](LICENSE)
@@ -0,0 +1,12 @@
1
+ import { ContinenteMcpServer } from './mcp/server.js';
2
+ import type { PromptHandler, ToolHandler } from './mcp/server.js';
3
+ export interface ApplicationOptions {
4
+ readonly version: string;
5
+ readonly tools?: readonly ToolHandler[];
6
+ readonly prompts?: readonly PromptHandler[];
7
+ readonly instructions?: string;
8
+ readonly closeResources?: () => Promise<void>;
9
+ readonly requestTimeoutMs?: number;
10
+ }
11
+ /** Create an MCP server from tool handlers. Importing the library performs no configuration or I/O. */
12
+ export declare function createApplication(options: ApplicationOptions): ContinenteMcpServer;
@@ -0,0 +1,5 @@
1
+ import { ContinenteMcpServer } from './mcp/server.js';
2
+ /** Create an MCP server from tool handlers. Importing the library performs no configuration or I/O. */
3
+ export function createApplication(options) {
4
+ return new ContinenteMcpServer({ ...options, tools: options.tools ?? [] });
5
+ }
@@ -0,0 +1,3 @@
1
+ import type { LoginOptions } from './clients/authentication.js';
2
+ /** Sign in with the configured credentials and save the session for the server to reuse. */
3
+ export declare function runAutoLogin(options?: LoginOptions): Promise<boolean>;
@@ -0,0 +1,10 @@
1
+ import { loadSettings } from './config.js';
2
+ import { FileCookieStore } from './stores/files.js';
3
+ import { AuthenticationClient } from './clients/authentication.js';
4
+ import { launchBrowser } from './runtime.js';
5
+ /** Sign in with the configured credentials and save the session for the server to reuse. */
6
+ export async function runAutoLogin(options = {}) {
7
+ const settings = loadSettings();
8
+ const logger = (message) => console.error(message);
9
+ return new AuthenticationClient(launchBrowser, new FileCookieStore(settings.cookieFile), settings, logger, options).loginWithCredentials();
10
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import type { ContinenteMcpServer } from './mcp/server.js';
3
+ export declare function startStdioServer(): Promise<ContinenteMcpServer>;
4
+ /** The SDK stdio transport does not treat input EOF as closure, so the CLI owns that lifecycle. */
5
+ export declare function connectStdioApplication(application: ContinenteMcpServer): Promise<void>;
6
+ export declare function main(args: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { createShoppingApplication } from './runtime.js';
8
+ import { runAutoLogin } from './auto-login.js';
9
+ import { isMainModule } from './main-module.js';
10
+ const USAGE = `Usage: continentept-mcp [command]
11
+
12
+ Commands:
13
+ (none) Start the MCP server on stdio
14
+ login Sign in with CONTINENTE_EMAIL and CONTINENTE_PASSWORD and save the session
15
+ (--show-browser displays the browser window)
16
+ install-browser Download the Chromium build used by the server (extra arguments go to Playwright)
17
+
18
+ Options:
19
+ -h, --help Show this help
20
+ -v, --version Show the version
21
+ `;
22
+ function packageVersion() {
23
+ const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
24
+ if (typeof metadata !== 'object' || metadata === null || !('version' in metadata) ||
25
+ typeof metadata.version !== 'string')
26
+ throw new Error('Missing package version');
27
+ return metadata.version;
28
+ }
29
+ export async function startStdioServer() {
30
+ const version = packageVersion();
31
+ const application = createShoppingApplication({ version });
32
+ await connectStdioApplication(application);
33
+ console.error(`continentept-mcp ${version} ready on stdio`);
34
+ return application;
35
+ }
36
+ /** The SDK stdio transport does not treat input EOF as closure, so the CLI owns that lifecycle. */
37
+ export async function connectStdioApplication(application) {
38
+ const detach = () => {
39
+ process.off('SIGINT', shutdown);
40
+ process.off('SIGTERM', shutdown);
41
+ process.stdin.off('end', shutdown);
42
+ process.stdin.off('close', shutdown);
43
+ };
44
+ const shutdown = () => {
45
+ detach();
46
+ void application.close().catch(() => { console.error('Could not close Continente MCP resources.'); process.exitCode = 1; });
47
+ };
48
+ process.once('SIGINT', shutdown);
49
+ process.once('SIGTERM', shutdown);
50
+ process.stdin.once('end', shutdown);
51
+ process.stdin.once('close', shutdown);
52
+ try {
53
+ await application.connect(new StdioServerTransport());
54
+ }
55
+ catch (error) {
56
+ detach();
57
+ throw error;
58
+ }
59
+ }
60
+ function installBrowser(extra) {
61
+ const playwright = dirname(createRequire(import.meta.url).resolve('playwright/package.json'));
62
+ const result = spawnSync(process.execPath, [join(playwright, 'cli.js'), 'install', 'chromium', ...extra], { stdio: 'inherit' });
63
+ return result.status ?? 1;
64
+ }
65
+ export async function main(args) {
66
+ const [command, ...rest] = args;
67
+ switch (command) {
68
+ case undefined:
69
+ case 'serve':
70
+ await startStdioServer();
71
+ return 0;
72
+ case 'login':
73
+ try {
74
+ return (await runAutoLogin({ showBrowser: rest.includes('--show-browser') })) ? 0 : 1;
75
+ }
76
+ catch (error) {
77
+ console.error(`Login failed: ${error instanceof Error ? error.message : 'unknown error'}`);
78
+ return 1;
79
+ }
80
+ case 'install-browser':
81
+ return installBrowser(rest);
82
+ case '-v':
83
+ case '--version':
84
+ console.log(packageVersion());
85
+ return 0;
86
+ case '-h':
87
+ case '--help':
88
+ console.log(USAGE);
89
+ return 0;
90
+ default:
91
+ console.error(`Unknown command: ${command}\n\n${USAGE}`);
92
+ return 2;
93
+ }
94
+ }
95
+ if (isMainModule(import.meta.url)) {
96
+ main(process.argv.slice(2)).then(code => { if (code)
97
+ process.exitCode = code; }, (error) => {
98
+ console.error(error instanceof Error ? error.message : 'Could not start Continente MCP');
99
+ process.exitCode = 1;
100
+ });
101
+ }
@@ -0,0 +1,26 @@
1
+ import type { BrowserContext, Frame, Page } from 'playwright';
2
+ import type { Logger } from '../ports.js';
3
+ import type { CookieStore } from '../stores/files.js';
4
+ import type { BrowserLauncher } from '../session/browser.js';
5
+ export interface Credentials {
6
+ readonly email: string;
7
+ readonly password: string;
8
+ }
9
+ export interface LoginOptions {
10
+ readonly showBrowser?: boolean;
11
+ }
12
+ /** Signs in through the website's login form and saves the resulting session cookies. */
13
+ export declare class AuthenticationClient {
14
+ private readonly launch;
15
+ private readonly cookies;
16
+ private readonly credentials;
17
+ private readonly logger;
18
+ private readonly options;
19
+ constructor(launch: BrowserLauncher, cookies: CookieStore, credentials: Credentials, logger: Logger, options?: LoginOptions);
20
+ /** Ask the storefront whether this browser context belongs to a signed-in customer. */
21
+ isSignedIn(context: BrowserContext): Promise<boolean>;
22
+ /** The login form is served in a frame from the login origin; wait for it to load. */
23
+ loginFrame(page: Page): Promise<Frame>;
24
+ private submit;
25
+ loginWithCredentials(): Promise<boolean>;
26
+ }
@@ -0,0 +1,92 @@
1
+ import { SessionCookiesSchema } from '../stores/files.js';
2
+ import { browserUserAgent } from '../session/browser.js';
3
+ import { field } from '../parsers/records.js';
4
+ import { STOREFRONT_ORIGIN } from '../parsers/catalog.js';
5
+ import { isLoginUrl, responseStatus } from './policy.js';
6
+ import { CART_SUMMARY_URL } from './storefront.js';
7
+ import { requestIO, acquireResource, checkRequest } from '../session/request.js';
8
+ const LOGIN_ORIGIN = 'https://login.continente.pt';
9
+ const EMAIL_INPUT = '#userNameCC';
10
+ const PASSWORD_INPUT = '#password_input';
11
+ const CONSENT_BUTTON = /^permitir todos$/i;
12
+ /** Signs in through the website's login form and saves the resulting session cookies. */
13
+ export class AuthenticationClient {
14
+ launch;
15
+ cookies;
16
+ credentials;
17
+ logger;
18
+ options;
19
+ constructor(launch, cookies, credentials, logger, options = {}) {
20
+ this.launch = launch;
21
+ this.cookies = cookies;
22
+ this.credentials = credentials;
23
+ this.logger = logger;
24
+ this.options = options;
25
+ }
26
+ /** Ask the storefront whether this browser context belongs to a signed-in customer. */
27
+ async isSignedIn(context) {
28
+ const response = await context.request.get(CART_SUMMARY_URL, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, timeout: 10000 });
29
+ if (!responseStatus(response, response.url?.() ?? CART_SUMMARY_URL).ok)
30
+ return false;
31
+ const body = await response.json().catch(() => null);
32
+ return field(body, 'resources', 'customerAuthenticated') === true;
33
+ }
34
+ /** The login form is served in a frame from the login origin; wait for it to load. */
35
+ async loginFrame(page) {
36
+ const fromLoginOrigin = (frame) => {
37
+ try {
38
+ return new URL(frame.url()).origin === LOGIN_ORIGIN;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ };
44
+ return page.frames().find(fromLoginOrigin) ?? page.waitForEvent('framenavigated', { predicate: fromLoginOrigin, timeout: 15000 });
45
+ }
46
+ async submit(frame, input, value) {
47
+ checkRequest();
48
+ const field = frame.locator(input);
49
+ await field.fill(value, { timeout: 15000 });
50
+ const button = frame.locator('button[type="submit"]:visible').first();
51
+ if (await button.count())
52
+ await button.click();
53
+ else
54
+ await field.press('Enter');
55
+ }
56
+ async loginWithCredentials() {
57
+ const { email, password } = this.credentials;
58
+ if (!email || !password) {
59
+ this.logger('Login skipped: CONTINENTE_EMAIL and CONTINENTE_PASSWORD are required.');
60
+ return false;
61
+ }
62
+ const browser = await acquireResource(() => this.launch({ headless: !this.options.showBrowser }), browser => browser.close());
63
+ let closing;
64
+ const close = () => closing ??= browser.close();
65
+ try {
66
+ return await requestIO(async () => {
67
+ const context = await acquireResource(() => browser.newContext({ userAgent: browserUserAgent(browser.version()), locale: 'pt-PT' }), context => context.close());
68
+ const page = await acquireResource(() => context.newPage(), page => page.close());
69
+ this.logger('Signing in.');
70
+ await page.goto(`${STOREFRONT_ORIGIN}/login/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
71
+ await page.getByRole('button', { name: CONSENT_BUTTON }).click({ timeout: 5000 }).catch(() => { });
72
+ const frame = await this.loginFrame(page);
73
+ await this.submit(frame, EMAIL_INPUT, email);
74
+ const leftLogin = page.waitForURL(url => url.origin === STOREFRONT_ORIGIN && !isLoginUrl(url.href), { timeout: 20000 }).catch(() => { });
75
+ await this.submit(frame, PASSWORD_INPUT, password);
76
+ await leftLogin;
77
+ checkRequest();
78
+ if (!(await this.isSignedIn(context)))
79
+ throw new Error('The website did not accept the login');
80
+ const cookies = SessionCookiesSchema.safeParse(await context.cookies());
81
+ if (!cookies.success)
82
+ throw new Error('The website returned an unexpected session');
83
+ await this.cookies.save(cookies.data);
84
+ this.logger('Signed in; session saved.');
85
+ return true;
86
+ }, close);
87
+ }
88
+ finally {
89
+ await close();
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,26 @@
1
+ export declare function isLoginUrl(value: string): boolean;
2
+ export interface HttpResponse {
3
+ ok(): boolean;
4
+ status(): number;
5
+ url?(): string;
6
+ headers?(): Record<string, string>;
7
+ }
8
+ /** Only known authentication evidence triggers recovery; arbitrary upstream failures do not. */
9
+ export declare function responseStatus(response: HttpResponse | null, finalUrl: string): import("../domain/result.js").Result<undefined, {
10
+ code: "cache_unavailable" | "cancelled" | "cart_change_rejected" | "cart_change_unconfirmed" | "deadline_exceeded" | "invalid_data" | "invalid_input" | "not_authenticated" | "not_found" | "order_details_unavailable" | "quantity_below_minimum" | "queue_full" | "sale_rules_unknown" | "server_closed" | "upstream_unavailable";
11
+ message: string;
12
+ retryable: boolean;
13
+ issues?: {
14
+ path: string;
15
+ code: string;
16
+ }[] | undefined;
17
+ }>;
18
+ export declare function authenticatedPayload(input: unknown): import("../domain/result.js").Result<unknown, {
19
+ code: "cache_unavailable" | "cancelled" | "cart_change_rejected" | "cart_change_unconfirmed" | "deadline_exceeded" | "invalid_data" | "invalid_input" | "not_authenticated" | "not_found" | "order_details_unavailable" | "quantity_below_minimum" | "queue_full" | "sale_rules_unknown" | "server_closed" | "upstream_unavailable";
20
+ message: string;
21
+ retryable: boolean;
22
+ issues?: {
23
+ path: string;
24
+ code: string;
25
+ }[] | undefined;
26
+ }>;
@@ -0,0 +1,37 @@
1
+ import { STOREFRONT_ORIGIN, storefrontUrl } from '../parsers/catalog.js';
2
+ import { field } from '../parsers/records.js';
3
+ import { failure, success } from '../domain/index.js';
4
+ export function isLoginUrl(value) {
5
+ try {
6
+ const url = new URL(value);
7
+ return url.protocol === 'https:' && (url.origin === 'https://login.continente.pt' ||
8
+ (url.origin === STOREFRONT_ORIGIN && /^\/login(?:\/|$)/i.test(url.pathname)));
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ /** Only known authentication evidence triggers recovery; arbitrary upstream failures do not. */
15
+ export function responseStatus(response, finalUrl) {
16
+ const status = response?.status();
17
+ const location = response?.headers?.().location;
18
+ let redirect = '';
19
+ try {
20
+ if (location)
21
+ redirect = new URL(location, STOREFRONT_ORIGIN).href;
22
+ }
23
+ catch {
24
+ return failure('invalid_data', 'Invalid response redirect');
25
+ }
26
+ if (status === 401 || status === 403 || isLoginUrl(finalUrl) || (status && status >= 300 && status < 400 && isLoginUrl(redirect))) {
27
+ return failure('not_authenticated');
28
+ }
29
+ if (!storefrontUrl(finalUrl))
30
+ return failure('invalid_data', 'Unexpected response origin');
31
+ if (status === 404)
32
+ return failure('not_found');
33
+ return response?.ok() ? success(undefined) : failure('upstream_unavailable', `The website returned HTTP ${status ?? 'with no status'}`);
34
+ }
35
+ export function authenticatedPayload(input) {
36
+ return field(input, 'resources', 'customerAuthenticated') === false ? failure('not_authenticated') : success(input);
37
+ }
@@ -0,0 +1,40 @@
1
+ import type { CatalogGateway, FavoritesGateway, CartGateway, OrdersGateway, PreparedCartMutation, RetrievedPages } from '../ports.js';
2
+ import type { Result, ToolError } from '../domain/index.js';
3
+ import type { BrowserLease, StorefrontSession } from '../session/browser.js';
4
+ export declare const CART_SUMMARY_URL = "https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Cart-MiniCartShow";
5
+ export declare class StorefrontClient {
6
+ readonly session: StorefrontSession;
7
+ constructor(session: StorefrontSession);
8
+ io<T>(lease: BrowserLease, operation: () => Promise<T>): Promise<T>;
9
+ navigateToStorefrontPage(path: string): Promise<Result<BrowserLease, ToolError>>;
10
+ html(path: string, marker?: string): Promise<Result<string, ToolError>>;
11
+ }
12
+ export declare class CatalogClient implements CatalogGateway {
13
+ private readonly site;
14
+ constructor(site: StorefrontClient);
15
+ fetchProductDetails(reference: string): Promise<Result<{
16
+ html: string;
17
+ url: string;
18
+ }, ToolError>>;
19
+ searchProducts(query: string, limit: number): Promise<Result<RetrievedPages, ToolError>>;
20
+ }
21
+ export declare class FavoritesClient implements FavoritesGateway {
22
+ private readonly site;
23
+ constructor(site: StorefrontClient);
24
+ fetchFavorites(): Promise<Result<RetrievedPages, ToolError>>;
25
+ }
26
+ export declare class CartClient implements CartGateway {
27
+ private readonly site;
28
+ constructor(site: StorefrontClient);
29
+ fetchCart(): Promise<Result<unknown, ToolError>>;
30
+ fetchProductForm(reference: string): Promise<Result<unknown, ToolError>>;
31
+ fetchCartControls(): Promise<Result<string, ToolError>>;
32
+ submitCartMutation(action: string, parameters: URLSearchParams, method: 'GET' | 'POST'): Promise<Result<unknown, ToolError>>;
33
+ prepareCartMutation(action: string, parameters: URLSearchParams, method: 'GET' | 'POST'): Promise<Result<PreparedCartMutation, ToolError>>;
34
+ }
35
+ export declare class OrdersClient implements OrdersGateway {
36
+ private readonly site;
37
+ constructor(site: StorefrontClient);
38
+ fetchOrderHistory(cursor?: string): Promise<Result<string, ToolError>>;
39
+ fetchOrderLines(url: string): Promise<Result<string, ToolError>>;
40
+ }