ebay-mcp-remote-edition 4.1.0 → 4.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.
- package/README.md +74 -5
- package/build/auth/kv-store.js +83 -24
- package/build/config/environment.js +8 -0
- package/build/scripts/bootstrap-ebay-research-session.js +130 -0
- package/build/scripts/check-playwright.js +31 -0
- package/build/scripts/env-check.js +20 -8
- package/build/scripts/inspect-ebay-research-session.js +49 -0
- package/build/scripts/playwright-runtime.js +5 -0
- package/build/server-http.js +82 -7
- package/build/validation/providers/ebay-research-session-alerts.js +963 -0
- package/build/validation/providers/ebay-research-session-store.js +506 -0
- package/build/validation/providers/ebay-research.js +1248 -191
- package/build/validation/providers/social.js +146 -14
- package/build/validation/providers/terapeak.js +122 -21
- package/build/validation/run-validation.js +150 -26
- package/build/validation/schemas.js +10 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -24,6 +24,9 @@ This project extends [Yosef Hayim's eBay MCP](https://github.com/YosefHayim/ebay
|
|
|
24
24
|
- **Cloudflare KV / Upstash Redis** token and session storage for persistent multi-user auth
|
|
25
25
|
- **Admin session management** — inspect, revoke, or delete sessions via authenticated endpoints
|
|
26
26
|
- **TTL-aligned records** — every stored record (OAuth state, auth code, session, user token) carries an `expiresAt` timestamp and a matching KV/Redis TTL so storage and application expiry are always in sync
|
|
27
|
+
- **Env-selected eBay Research session persistence** — the first-party research bootstrap/runtime can persist Playwright storage state to Cloudflare KV, Upstash KV, or explicit filesystem mode via `EBAY_RESEARCH_SESSION_STORE`
|
|
28
|
+
- **QStash-triggered Telegram alerts for eBay Research session expiry** — bootstrap can schedule version-aware expiry callbacks that notify operators before first-party research auth silently degrades
|
|
29
|
+
- **Alert-safe scheduling guardrails** — expiry callbacks are only scheduled when the callback URL is externally reachable and the research session store supports shared alert locks (`upstash-redis` or `filesystem`)
|
|
27
30
|
|
|
28
31
|
---
|
|
29
32
|
|
|
@@ -295,6 +298,24 @@ CLOUDFLARE_API_TOKEN=
|
|
|
295
298
|
UPSTASH_REDIS_REST_URL=
|
|
296
299
|
UPSTASH_REDIS_REST_TOKEN=
|
|
297
300
|
|
|
301
|
+
# eBay Research session-expiry alerts (optional but recommended when using
|
|
302
|
+
# first-party research in hosted mode)
|
|
303
|
+
TELEGRAM_BOT_TOKEN=
|
|
304
|
+
TELEGRAM_CHAT_ID=1574052684
|
|
305
|
+
QSTASH_URL=
|
|
306
|
+
QSTASH_TOKEN=
|
|
307
|
+
QSTASH_CURRENT_SIGNING_KEY=
|
|
308
|
+
QSTASH_NEXT_SIGNING_KEY=
|
|
309
|
+
EBAY_RESEARCH_SESSION_ALERTS_ENABLED=true
|
|
310
|
+
EBAY_RESEARCH_SESSION_ALERT_WINDOW_24H=true
|
|
311
|
+
EBAY_RESEARCH_SESSION_ALERT_WINDOW_6H=true
|
|
312
|
+
EBAY_RESEARCH_SESSION_ALERT_ON_EXPIRED=true
|
|
313
|
+
EBAY_RESEARCH_SESSION_ALERT_CALLBACK_URL=
|
|
314
|
+
|
|
315
|
+
# Alert scheduling additionally requires:
|
|
316
|
+
# - PUBLIC_BASE_URL or EBAY_RESEARCH_SESSION_ALERT_CALLBACK_URL to be externally reachable
|
|
317
|
+
# - EBAY_RESEARCH_SESSION_STORE=upstash-redis or filesystem
|
|
318
|
+
|
|
298
319
|
# Security
|
|
299
320
|
ADMIN_API_KEY= # required for admin session endpoints
|
|
300
321
|
OAUTH_START_KEY= # optional; protects /oauth/start with a shared secret
|
|
@@ -358,7 +379,7 @@ Store eBay credentials in a mounted secret file rather than raw environment vari
|
|
|
358
379
|
}
|
|
359
380
|
```
|
|
360
381
|
|
|
361
|
-
### Deploy to Render
|
|
382
|
+
### Deploy to Render / Railway / other Nixpacks hosts
|
|
362
383
|
|
|
363
384
|
1. Connect your repo to Render as a **Web Service**
|
|
364
385
|
2. Set **Build command:**
|
|
@@ -374,6 +395,12 @@ Store eBay credentials in a mounted secret file rather than raw environment vari
|
|
|
374
395
|
|
|
375
396
|
The server starts on the port Render assigns via `$PORT` and logs the active KV backend on startup.
|
|
376
397
|
|
|
398
|
+
For Nixpacks-based platforms such as Railway and Coolify:
|
|
399
|
+
|
|
400
|
+
- The repository now includes `nixpacks.toml` so the generated image uses `pnpm install --frozen-lockfile`, runs `pnpm run build`, installs Chromium for the Playwright-backed validation paths, and starts with `pnpm run start:http`.
|
|
401
|
+
- Runtime secrets should be configured in the platform dashboard as runtime environment variables or mounted secret files. Do not bake secrets into Docker build arguments or `ENV` layers.
|
|
402
|
+
- Keep `pnpm-lock.yaml` committed and in sync with `package.json`; Nixpacks installs with a frozen lockfile.
|
|
403
|
+
|
|
377
404
|
### OAuth flows
|
|
378
405
|
|
|
379
406
|
eBay requires a single registered callback URL per application. The hosted server registers `/oauth/callback` at the root and recovers the environment from the stored OAuth state record.
|
|
@@ -593,6 +620,7 @@ GET /health
|
|
|
593
620
|
GET /whoami
|
|
594
621
|
GET /sandbox/validation/health
|
|
595
622
|
GET /production/validation/health
|
|
623
|
+
POST /internal/ebay-research/check-session-expiry
|
|
596
624
|
```
|
|
597
625
|
|
|
598
626
|
Recommended debugging flow:
|
|
@@ -600,6 +628,7 @@ Recommended debugging flow:
|
|
|
600
628
|
1. Call `/health` to confirm the HTTP service is up.
|
|
601
629
|
2. Call `/whoami` with a Bearer hosted session token to confirm the active hosted user session, bound environment, expiry, and revocation status.
|
|
602
630
|
3. Call the matching env-scoped `/validation/health` route with `X-Admin-API-Key` to confirm the validation runner user is configured, stored tokens exist, and token refresh succeeds.
|
|
631
|
+
4. The internal `POST /internal/ebay-research/check-session-expiry` route is reserved for signed QStash callbacks and should not be used as an unauthenticated public endpoint.
|
|
603
632
|
|
|
604
633
|
`/whoami` is especially useful when an operator wants to verify which hosted session is currently active before registering or troubleshooting the validation runner user. Validation routes themselves still authenticate with the admin key and a stored hosted runner identity, not with MCP auth.
|
|
605
634
|
|
|
@@ -623,7 +652,7 @@ Provider behavior:
|
|
|
623
652
|
- **Browse debug semantics:** validation debug now keeps browse candidate generation, selected query/tier, browse-specific sample size, and per-candidate result counts separate from sold-provider result counts so operators can tell whether the browse layer contributed a field, fell back to a weaker query, or returned no usable match.
|
|
624
653
|
- **Sold provider:** [`src/validation/providers/ebay-sold.ts`](src/validation/providers/ebay-sold.ts) uses a temporary external sold-data source configured by `SOLD_ITEMS_API_URL` and `SOLD_ITEMS_API_KEY`. It uses the same query-fallback strategy as the browse provider and returns sold-price ranges, sample sold items, and recent sold-velocity buckets when available.
|
|
625
654
|
- **Terapeak / eBay research provider:** [`src/validation/providers/terapeak.ts`](src/validation/providers/terapeak.ts) now evaluates authenticated eBay Research candidates for both current-market and previous-POB contexts, scores them against title alignment and subtype coverage, preserves per-candidate diagnostics in debug output, and derives sold-day buckets from sold-row timestamps when available.
|
|
626
|
-
- **Authenticated research session source:** [`src/validation/providers/ebay-research.ts`](src/validation/providers/ebay-research.ts)
|
|
655
|
+
- **Authenticated research session source:** [`src/validation/providers/ebay-research.ts`](src/validation/providers/ebay-research.ts) now prefers KV-backed Playwright storage state first, then environment-provided storage state / cookie fallbacks, then local storage-state/profile fallbacks for local development only. Parsed ACTIVE and SOLD tab responses are cached, automatically invalidated when the authenticated cookie fingerprint changes, and emit explicit auth-resolution debug fields including `sessionSource`, KV/env/filesystem attempt status, and fallback reasons.
|
|
627
656
|
- **Social provider:** [`src/validation/providers/social.ts`](src/validation/providers/social.ts) supports phase-1 Twitter/X recent activity, YouTube average-daily-views proxy data exposed through the `youtubeViews24hMillions` field, and Reddit recent post counts. These signals degrade gracefully on provider/API failure and are used as supportive indicators rather than authoritative demand truth.
|
|
628
657
|
- **Chart provider:** [`src/validation/providers/chart.ts`](src/validation/providers/chart.ts) is still a stub and does not currently contribute chart-based metrics.
|
|
629
658
|
- **Previous comeback research provider:** [`src/validation/providers/research.ts`](src/validation/providers/research.ts) now performs Perplexity-backed historical research when `PERPLEXITY_API_KEY` is configured. It attempts to resolve the prior comeback, normalize previous first-week sales when support exists, assign a `perplexityHistoricalContextScore`, generate concise `historicalContextNotes`, and emit debug diagnostics covering the research query, citations/snippets, resolved prior release, confidence, and score reasoning.
|
|
@@ -640,12 +669,27 @@ Known limitations in the current implementation:
|
|
|
640
669
|
- The sold-data provider depends on external configuration via `SOLD_ITEMS_API_URL` and `SOLD_ITEMS_API_KEY`.
|
|
641
670
|
- If those sold-data variables are missing, validation still runs but sold enrichment degrades to an unavailable/error state rather than providing full historical-sales signals.
|
|
642
671
|
- The sold-data provider is temporary and intended to be replaced by an internal implementation later.
|
|
643
|
-
- Authenticated eBay Research requires a valid session source such as `EBAY_RESEARCH_COOKIES_JSON`, a
|
|
672
|
+
- Authenticated eBay Research requires a valid session source such as KV-backed Playwright storage state, `EBAY_RESEARCH_STORAGE_STATE_JSON`, `EBAY_RESEARCH_COOKIES_JSON`, a local Playwright storage-state file, or a local browser profile directory; without one, the provider degrades to diagnostic-only output with explicit structured auth-resolution debug.
|
|
673
|
+
|
|
674
|
+
#### eBay Research bootstrap and hosted runtime notes
|
|
675
|
+
|
|
676
|
+
- Install Chromium for hosted runtimes with [`package.json`](package.json) script `playwright:install` (`pnpm run playwright:install`).
|
|
677
|
+
- The Docker deployment path now provisions Chromium during image build in [`Dockerfile`](Dockerfile).
|
|
678
|
+
- Canonical production session source of truth is KV-backed Playwright storage-state JSON stored under `ebay_research_storage_state_json` with companion metadata in `ebay_research_storage_state_meta`, including `updatedAt`, `expiresAt`, `ttlSeconds`, `marketplace`, `sessionStore`, and `sessionVersion`.
|
|
679
|
+
- Bootstrap a signed-in eBay Research storage state into KV with [`src/scripts/bootstrap-ebay-research-session.ts`](src/scripts/bootstrap-ebay-research-session.ts) via the packaged/runtime-safe [`package.json`](package.json) script `research:bootstrap` (`pnpm run build && pnpm run research:bootstrap`).
|
|
680
|
+
- Inspect canonical eBay Research session persistence and fresh-client readback diagnostics with [`src/scripts/inspect-ebay-research-session.ts`](src/scripts/inspect-ebay-research-session.ts) via the packaged/runtime-safe [`package.json`](package.json) script `research:inspect-session` (`pnpm run build && pnpm run research:inspect-session`).
|
|
681
|
+
- Verify headless Chromium launchability with [`src/scripts/check-playwright.ts`](src/scripts/check-playwright.ts) via the packaged/runtime-safe [`package.json`](package.json) script `research:check-browser` (`pnpm run build && pnpm run research:check-browser`).
|
|
682
|
+
- Runtime precedence is: KV storage state → `EBAY_RESEARCH_STORAGE_STATE_JSON` → `EBAY_RESEARCH_COOKIES_JSON` → local storage-state file → local Playwright profile → explicit auth-missing fallback.
|
|
683
|
+
- Every candidate session source is validated against the first-party ACTIVE endpoint before the provider reports `authState = loaded`; failed validation is surfaced through debug fields including `kvStorageStateBytes`, `authValidationAttempted`, and `authValidationSucceeded`.
|
|
684
|
+
- Once a validated session is loaded, ACTIVE and SOLD endpoint fetches automatically become the preferred first-party research source while legacy active/sold fallbacks remain intact when auth is missing or invalid.
|
|
685
|
+
- Successful bootstrap also schedules signed QStash callbacks for 24 hours before expiry, 6 hours before expiry, and at expiry. Those callbacks target `POST /internal/ebay-research/check-session-expiry`, which verifies QStash signatures, suppresses stale reminders by `sessionVersion`, and sends Telegram alerts to `TELEGRAM_CHAT_ID`.
|
|
686
|
+
- Alert scheduling is intentionally skipped when the callback URL resolves to localhost/loopback or when `EBAY_RESEARCH_SESSION_STORE` uses a backend without shared lock support, because those configurations cannot safely deliver or deduplicate hosted reminders.
|
|
687
|
+
- Session refresh is manual by design for now: rerun `pnpm run build && pnpm run research:bootstrap` whenever eBay expires the stored session, then redeploy or restart the hosted service if your platform does not hot-reload env/KV-backed state.
|
|
644
688
|
- The previous-comeback research provider depends on grounded external research and therefore degrades to low-confidence notes with a zero historical score when `PERPLEXITY_API_KEY` is missing, the response cannot be normalized, or reliable evidence is not found.
|
|
645
689
|
- The browse provider still relies on heuristic query selection and fallback matching.
|
|
646
690
|
- The YouTube-backed `youtubeViews24hMillions` field is currently an **average daily views proxy**, not a true trailing 24-hour delta.
|
|
647
691
|
- Social signals are supportive/proxy data only and should not be presented as decisive automated buy logic.
|
|
648
|
-
- eBay-derived metrics are intentionally practical rather than exhaustive
|
|
692
|
+
- eBay-derived metrics are intentionally practical rather than exhaustive, but authenticated ACTIVE research rows now populate watcher-derived metrics whenever watcher counts are present in the first-party response.
|
|
649
693
|
|
|
650
694
|
### Roadmap note: provider maturation
|
|
651
695
|
|
|
@@ -765,7 +809,7 @@ Review the diff, commit the generated changes you want to keep, and deploy.
|
|
|
765
809
|
|
|
766
810
|
### Local env management
|
|
767
811
|
|
|
768
|
-
For local development, standard runtime scripts
|
|
812
|
+
For local development, standard runtime scripts load `.env` via dotenvx only when a real local [`.env`](.env) file is present. Hosted platforms should provide environment variables directly — the server skips dotenvx for hosted/runtime environments (including Nixpacks-style deployments, which set [`DISABLE_DOTENVX`](nixpacks.toml:26)) and whenever no local [`.env`](.env) file exists.
|
|
769
813
|
|
|
770
814
|
```bash
|
|
771
815
|
pnpm run env:encrypt # encrypt .env for safe sharing
|
|
@@ -871,6 +915,31 @@ Common causes:
|
|
|
871
915
|
- `SOLD_ITEMS_API_URL` or `SOLD_ITEMS_API_KEY` is missing, causing sold enrichment to degrade
|
|
872
916
|
- one or more social-provider credentials are absent, which causes the related supportive signal to degrade gracefully instead of failing the entire run
|
|
873
917
|
|
|
918
|
+
### eBay Research debug shows `authState = missing` or `sessionStrategy = none`
|
|
919
|
+
|
|
920
|
+
This means the first-party research provider could not load a validated authenticated session and validation is intentionally falling back to browse and/or the temporary sold provider.
|
|
921
|
+
|
|
922
|
+
Run this checklist:
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
pnpm run playwright:install
|
|
926
|
+
pnpm run build
|
|
927
|
+
pnpm run research:check-browser
|
|
928
|
+
pnpm run research:bootstrap
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
Expected post-bootstrap debug characteristics from [`src/validation/providers/ebay-research.ts`](src/validation/providers/ebay-research.ts):
|
|
932
|
+
|
|
933
|
+
- `authState = loaded`
|
|
934
|
+
- `sessionStrategy = storage_state`
|
|
935
|
+
- `sessionSource = kv`
|
|
936
|
+
- `kvLoadAttempted = true`
|
|
937
|
+
- `kvLoadSucceeded = true`
|
|
938
|
+
- `authValidationAttempted = true`
|
|
939
|
+
- `authValidationSucceeded = true`
|
|
940
|
+
|
|
941
|
+
If the provider still reports `missing`, verify that your hosted deployment can reach the configured KV backend, that Chromium is available in the runtime image, and that the stored eBay session has not expired. Refresh the session by rerunning `pnpm run research:bootstrap`.
|
|
942
|
+
|
|
874
943
|
If `authDebug.tokenEndpoint` or the captured upstream response looks wrong, verify the environment-specific OAuth configuration and token-base resolution.
|
|
875
944
|
|
|
876
945
|
### Security checklist
|
package/build/auth/kv-store.js
CHANGED
|
@@ -26,6 +26,20 @@ export class InMemoryKVStore {
|
|
|
26
26
|
});
|
|
27
27
|
return Promise.resolve();
|
|
28
28
|
}
|
|
29
|
+
putIfAbsent(key, value, expirationTtl) {
|
|
30
|
+
const entry = this.store.get(key);
|
|
31
|
+
if (entry && (entry.expiresAt === undefined || Date.now() <= entry.expiresAt)) {
|
|
32
|
+
return Promise.resolve(false);
|
|
33
|
+
}
|
|
34
|
+
if (entry?.expiresAt !== undefined && Date.now() > entry.expiresAt) {
|
|
35
|
+
this.store.delete(key);
|
|
36
|
+
}
|
|
37
|
+
this.store.set(key, {
|
|
38
|
+
value,
|
|
39
|
+
expiresAt: expirationTtl !== undefined ? Date.now() + expirationTtl * 1_000 : undefined,
|
|
40
|
+
});
|
|
41
|
+
return Promise.resolve(true);
|
|
42
|
+
}
|
|
29
43
|
delete(key) {
|
|
30
44
|
this.store.delete(key);
|
|
31
45
|
return Promise.resolve();
|
|
@@ -97,6 +111,9 @@ export class CloudflareKVStore {
|
|
|
97
111
|
: this.cacheTtlMs;
|
|
98
112
|
this.cache.set(key, { value, expiresAt: Date.now() + cacheTtl });
|
|
99
113
|
}
|
|
114
|
+
putIfAbsent() {
|
|
115
|
+
throw new Error('Atomic putIfAbsent is not supported for Cloudflare KV');
|
|
116
|
+
}
|
|
100
117
|
async delete(key) {
|
|
101
118
|
await this.client.delete(`/values/${encodeURIComponent(key)}`);
|
|
102
119
|
this.cache.delete(key);
|
|
@@ -153,6 +170,19 @@ export class UpstashRedisKVStore {
|
|
|
153
170
|
: this.cacheTtlMs;
|
|
154
171
|
this.cache.set(key, { value, expiresAt: Date.now() + cacheTtl });
|
|
155
172
|
}
|
|
173
|
+
async putIfAbsent(key, value, expirationTtl) {
|
|
174
|
+
const response = expirationTtl !== undefined
|
|
175
|
+
? await this.client.set(key, value, { ex: expirationTtl, nx: true })
|
|
176
|
+
: await this.client.set(key, value, { nx: true });
|
|
177
|
+
if (response === 'OK' || response === true) {
|
|
178
|
+
const cacheTtl = expirationTtl
|
|
179
|
+
? Math.min(expirationTtl * 1_000, this.cacheTtlMs)
|
|
180
|
+
: this.cacheTtlMs;
|
|
181
|
+
this.cache.set(key, { value, expiresAt: Date.now() + cacheTtl });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
156
186
|
async delete(key) {
|
|
157
187
|
await this.client.del(key);
|
|
158
188
|
this.cache.delete(key);
|
|
@@ -180,6 +210,54 @@ export class UpstashRedisKVStore {
|
|
|
180
210
|
* that need to swap the backend.
|
|
181
211
|
*/
|
|
182
212
|
let _kvStoreSingleton = null;
|
|
213
|
+
const _kvStoreSingletonsByBackend = new Map();
|
|
214
|
+
function normalizeKVStoreBackend(value) {
|
|
215
|
+
const backend = (value ?? 'cloudflare-kv').toLowerCase().trim();
|
|
216
|
+
switch (backend) {
|
|
217
|
+
case 'memory':
|
|
218
|
+
case 'in-memory':
|
|
219
|
+
return 'memory';
|
|
220
|
+
case 'upstash-redis':
|
|
221
|
+
case 'upstash':
|
|
222
|
+
case 'redis':
|
|
223
|
+
return 'upstash-redis';
|
|
224
|
+
case 'cloudflare-kv':
|
|
225
|
+
case 'cloudflare':
|
|
226
|
+
default:
|
|
227
|
+
return 'cloudflare-kv';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function createKVStoreInstance(backend, rawEnv) {
|
|
231
|
+
switch (backend) {
|
|
232
|
+
case 'memory': {
|
|
233
|
+
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? ''}" → using InMemoryKVStore (no external KV calls)`);
|
|
234
|
+
return new InMemoryKVStore();
|
|
235
|
+
}
|
|
236
|
+
case 'upstash-redis': {
|
|
237
|
+
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? ''}" → using UpstashRedisKVStore (url="${process.env.UPSTASH_REDIS_REST_URL ?? '(unset)'}")`);
|
|
238
|
+
return new UpstashRedisKVStore();
|
|
239
|
+
}
|
|
240
|
+
case 'cloudflare-kv':
|
|
241
|
+
default: {
|
|
242
|
+
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? '(unset)'}" → using CloudflareKVStore (accountId="${process.env.CLOUDFLARE_ACCOUNT_ID ?? '(unset)'}") — set EBAY_TOKEN_STORE_BACKEND=memory to disable`);
|
|
243
|
+
return new CloudflareKVStore();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function createFreshKVStoreForBackend(backend) {
|
|
248
|
+
const normalizedBackend = normalizeKVStoreBackend(backend);
|
|
249
|
+
return createKVStoreInstance(normalizedBackend, backend);
|
|
250
|
+
}
|
|
251
|
+
export function createKVStoreForBackend(backend) {
|
|
252
|
+
const normalizedBackend = normalizeKVStoreBackend(backend);
|
|
253
|
+
const existing = _kvStoreSingletonsByBackend.get(normalizedBackend);
|
|
254
|
+
if (existing) {
|
|
255
|
+
return existing;
|
|
256
|
+
}
|
|
257
|
+
const store = createKVStoreInstance(normalizedBackend, backend);
|
|
258
|
+
_kvStoreSingletonsByBackend.set(normalizedBackend, store);
|
|
259
|
+
return store;
|
|
260
|
+
}
|
|
183
261
|
/**
|
|
184
262
|
* Returns (or lazily creates) the process-wide KV store singleton based on the
|
|
185
263
|
* EBAY_TOKEN_STORE_BACKEND environment variable:
|
|
@@ -196,30 +274,10 @@ export function createKVStore() {
|
|
|
196
274
|
return _kvStoreSingleton;
|
|
197
275
|
}
|
|
198
276
|
const rawEnv = process.env.EBAY_TOKEN_STORE_BACKEND;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
case 'in-memory': {
|
|
204
|
-
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? ''}" → using InMemoryKVStore (no external KV calls)`);
|
|
205
|
-
_kvStoreSingleton = new InMemoryKVStore();
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
case 'upstash-redis':
|
|
209
|
-
case 'upstash':
|
|
210
|
-
case 'redis': {
|
|
211
|
-
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? ''}" → using UpstashRedisKVStore (url="${process.env.UPSTASH_REDIS_REST_URL ?? '(unset)'}")`);
|
|
212
|
-
_kvStoreSingleton = new UpstashRedisKVStore();
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
case 'cloudflare-kv':
|
|
216
|
-
case 'cloudflare':
|
|
217
|
-
default: {
|
|
218
|
-
console.log(`[kv-store] EBAY_TOKEN_STORE_BACKEND="${rawEnv ?? '(unset)'}" → using CloudflareKVStore (accountId="${process.env.CLOUDFLARE_ACCOUNT_ID ?? '(unset)'}") — set EBAY_TOKEN_STORE_BACKEND=memory to disable`);
|
|
219
|
-
_kvStoreSingleton = new CloudflareKVStore();
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
277
|
+
const normalizedBackend = normalizeKVStoreBackend(rawEnv);
|
|
278
|
+
_kvStoreSingleton =
|
|
279
|
+
_kvStoreSingletonsByBackend.get(normalizedBackend) ??
|
|
280
|
+
createKVStoreForBackend(rawEnv ?? normalizedBackend);
|
|
223
281
|
return _kvStoreSingleton;
|
|
224
282
|
}
|
|
225
283
|
/**
|
|
@@ -234,4 +292,5 @@ export function createKVStore() {
|
|
|
234
292
|
*/
|
|
235
293
|
export function resetKVStoreSingleton(replacement = null) {
|
|
236
294
|
_kvStoreSingleton = replacement;
|
|
295
|
+
_kvStoreSingletonsByBackend.clear();
|
|
237
296
|
}
|
|
@@ -296,6 +296,14 @@ export function getValidationRunnerUserId(environment) {
|
|
|
296
296
|
const resolved = (envSpecific ?? process.env.VALIDATION_RUNNER_USER_ID ?? '').trim();
|
|
297
297
|
return resolved || null;
|
|
298
298
|
}
|
|
299
|
+
export function getTelegramChatId() {
|
|
300
|
+
const value = (process.env.TELEGRAM_CHAT_ID ?? '').trim();
|
|
301
|
+
return value || null;
|
|
302
|
+
}
|
|
303
|
+
export function getTelegramBotToken() {
|
|
304
|
+
const value = (process.env.TELEGRAM_BOT_TOKEN ?? '').trim();
|
|
305
|
+
return value || null;
|
|
306
|
+
}
|
|
299
307
|
export function getBaseUrl(environment) {
|
|
300
308
|
return environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
|
|
301
309
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { clearEbayResearchAuthCache, inspectEbayResearchAuthState, inspectEbayResearchSessionPersistence, storeEbayResearchSessionToKv, } from '../validation/providers/ebay-research.js';
|
|
4
|
+
import { createEbayResearchSessionStoreResolution, } from '../validation/providers/ebay-research-session-store.js';
|
|
5
|
+
import { scheduleEbayResearchSessionAlerts } from '../validation/providers/ebay-research-session-alerts.js';
|
|
6
|
+
import { loadChromium } from './playwright-runtime.js';
|
|
7
|
+
const configuredMarketplace = process.env.EBAY_RESEARCH_BOOTSTRAP_MARKETPLACE?.trim();
|
|
8
|
+
const marketplace = configuredMarketplace && configuredMarketplace.length > 0 ? configuredMarketplace : 'EBAY-US';
|
|
9
|
+
const researchUrl = `https://www.ebay.com/sh/research?marketplace=${encodeURIComponent(marketplace)}`;
|
|
10
|
+
function getExpectedVerificationSessionSource(selectedStore) {
|
|
11
|
+
if (selectedStore === 'cloudflare_kv' || selectedStore === 'upstash-redis') {
|
|
12
|
+
return 'kv';
|
|
13
|
+
}
|
|
14
|
+
if (selectedStore === 'filesystem') {
|
|
15
|
+
return 'filesystem';
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function getChromiumChannel() {
|
|
20
|
+
const configuredChannel = process.env.PLAYWRIGHT_CHROMIUM_CHANNEL?.trim();
|
|
21
|
+
return configuredChannel && configuredChannel.length > 0 ? configuredChannel : undefined;
|
|
22
|
+
}
|
|
23
|
+
function alertingLooksConfigured() {
|
|
24
|
+
return [
|
|
25
|
+
process.env.QSTASH_URL,
|
|
26
|
+
process.env.QSTASH_TOKEN,
|
|
27
|
+
process.env.QSTASH_CURRENT_SIGNING_KEY,
|
|
28
|
+
process.env.QSTASH_NEXT_SIGNING_KEY,
|
|
29
|
+
process.env.TELEGRAM_BOT_TOKEN,
|
|
30
|
+
process.env.TELEGRAM_CHAT_ID,
|
|
31
|
+
process.env.EBAY_RESEARCH_SESSION_ALERT_CALLBACK_URL,
|
|
32
|
+
process.env.PUBLIC_BASE_URL,
|
|
33
|
+
].some((value) => typeof value === 'string' && value.trim().length > 0);
|
|
34
|
+
}
|
|
35
|
+
async function waitForEnter(promptText) {
|
|
36
|
+
await new Promise((resolve) => {
|
|
37
|
+
const rl = createInterface({ input, output });
|
|
38
|
+
rl.question(promptText, () => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const chromium = await loadChromium();
|
|
46
|
+
const browser = await chromium.launch({
|
|
47
|
+
headless: false,
|
|
48
|
+
channel: getChromiumChannel(),
|
|
49
|
+
});
|
|
50
|
+
const context = await browser.newContext();
|
|
51
|
+
const page = await context.newPage();
|
|
52
|
+
try {
|
|
53
|
+
console.log(`Opening eBay Research bootstrap flow for marketplace ${marketplace}...`);
|
|
54
|
+
await page.goto(researchUrl, { waitUntil: 'domcontentloaded' });
|
|
55
|
+
await waitForEnter('Sign in to eBay Research in the opened browser window, confirm the research UI is accessible, then press Enter to persist storage state to KV. ');
|
|
56
|
+
await page.goto(researchUrl, { waitUntil: 'domcontentloaded' });
|
|
57
|
+
const currentUrl = page.url();
|
|
58
|
+
if (!currentUrl.includes('/sh/research')) {
|
|
59
|
+
throw new Error(`Research UI access could not be confirmed before persistence (currentUrl=${currentUrl}).`);
|
|
60
|
+
}
|
|
61
|
+
const storageState = await context.storageState();
|
|
62
|
+
await storeEbayResearchSessionToKv(marketplace, storageState, 'storage_state');
|
|
63
|
+
clearEbayResearchAuthCache();
|
|
64
|
+
const persistence = await inspectEbayResearchSessionPersistence(marketplace);
|
|
65
|
+
console.log(`[eBayResearchSessionBootstrap] wrote storage state to ${persistence.sessionStoreSelected} key=${persistence.canonicalStateKey ?? 'null'} bytes=${persistence.storageStateBytes}`);
|
|
66
|
+
console.log(`[eBayResearchSessionBootstrap] canonical storage-state key ${persistence.canonicalStateKey ?? 'null'} exists=${persistence.storageStateExists} bytes=${persistence.storageStateBytes} valid=${persistence.storageStateValid}`);
|
|
67
|
+
console.log(`[eBayResearchSessionBootstrap] fresh-client canonical key ${persistence.freshCanonicalReadback.key ?? 'null'} exists=${persistence.freshCanonicalReadback.exists} type=${persistence.freshCanonicalReadback.valueType} bytes=${persistence.freshCanonicalReadback.bytes} valid=${persistence.freshCanonicalReadback.validPlaywrightStorageStateJson} configuredFrom=${persistence.freshCanonicalReadback.configuredFrom} scope=${persistence.freshCanonicalReadback.stateKeyScope} connection=${persistence.freshCanonicalReadback.connection ?? 'null'} credentialFingerprint=${persistence.freshCanonicalReadback.credentialFingerprint ?? 'null'} error=${persistence.freshCanonicalReadback.error ?? 'null'}`);
|
|
68
|
+
console.log(`[eBayResearchSessionBootstrap] metadata key ${persistence.canonicalMetaKey ?? 'null'} exists=${persistence.metadataExists}`);
|
|
69
|
+
if (persistence.storeTargetConnection) {
|
|
70
|
+
console.log(`[eBayResearchSessionBootstrap] target=${persistence.storeTargetConnection} credentialsConfigured=${persistence.storeCredentialsConfigured}`);
|
|
71
|
+
}
|
|
72
|
+
const verification = await inspectEbayResearchAuthState(marketplace);
|
|
73
|
+
const expectedSessionSource = getExpectedVerificationSessionSource(persistence.sessionStoreSelected);
|
|
74
|
+
if (verification.authState !== 'loaded' ||
|
|
75
|
+
verification.sessionSource !== expectedSessionSource ||
|
|
76
|
+
verification.authValidationSucceeded !== true) {
|
|
77
|
+
throw new Error(`eBay Research session bootstrap verification failed (authState=${verification.authState}, sessionSource=${verification.sessionSource ?? 'none'}, authValidationSucceeded=${verification.authValidationSucceeded}, cookieCount=${verification.cookieCount}).`);
|
|
78
|
+
}
|
|
79
|
+
const latestStoreResolution = createEbayResearchSessionStoreResolution(marketplace);
|
|
80
|
+
const meta = latestStoreResolution.store ? await latestStoreResolution.store.getMeta() : null;
|
|
81
|
+
if (typeof meta?.expiresAt === 'string' && typeof meta?.sessionVersion === 'string') {
|
|
82
|
+
const scheduleResult = await scheduleEbayResearchSessionAlerts({
|
|
83
|
+
marketplace,
|
|
84
|
+
expiresAt: meta.expiresAt,
|
|
85
|
+
sessionVersion: meta.sessionVersion,
|
|
86
|
+
});
|
|
87
|
+
console.log(`[eBayResearchSessionAlerts] schedule status=${scheduleResult.status} reason=${scheduleResult.reason ?? 'none'} callbackUrl=${scheduleResult.callbackUrl} entries=${scheduleResult.scheduled.length}`);
|
|
88
|
+
for (const entry of scheduleResult.scheduled) {
|
|
89
|
+
console.log(`[eBayResearchSessionAlerts] scheduled threshold=${entry.threshold} targetTime=${entry.targetTime} messageId=${entry.messageId ?? 'null'}`);
|
|
90
|
+
}
|
|
91
|
+
if (scheduleResult.status === 'skipped' &&
|
|
92
|
+
alertingLooksConfigured() &&
|
|
93
|
+
(scheduleResult.reason === 'shared_lock_backend_unavailable' ||
|
|
94
|
+
scheduleResult.reason === 'callback_url_not_public' ||
|
|
95
|
+
scheduleResult.reason === 'callback_url_invalid')) {
|
|
96
|
+
throw new Error(scheduleResult.reason === 'shared_lock_backend_unavailable'
|
|
97
|
+
? 'eBay Research session alerts require EBAY_RESEARCH_SESSION_STORE=upstash-redis or filesystem because the current backend cannot provide shared alert locks.'
|
|
98
|
+
: 'eBay Research session alerts require a publicly reachable callback URL. Set PUBLIC_BASE_URL or EBAY_RESEARCH_SESSION_ALERT_CALLBACK_URL to an externally reachable URL before bootstrapping.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log(JSON.stringify({
|
|
102
|
+
ok: true,
|
|
103
|
+
marketplace,
|
|
104
|
+
persistedTo: persistence.sessionStoreSelected,
|
|
105
|
+
storeTargetConnection: persistence.storeTargetConnection,
|
|
106
|
+
storeCredentialsConfigured: persistence.storeCredentialsConfigured,
|
|
107
|
+
canonicalKeys: {
|
|
108
|
+
storageState: persistence.canonicalStateKey,
|
|
109
|
+
metadata: persistence.canonicalMetaKey,
|
|
110
|
+
},
|
|
111
|
+
sessionMetadata: meta,
|
|
112
|
+
persistence,
|
|
113
|
+
inspectCommand: 'pnpm run research:inspect-session',
|
|
114
|
+
refreshCommand: 'pnpm run research:bootstrap',
|
|
115
|
+
verification,
|
|
116
|
+
}, null, 2));
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await context.close();
|
|
120
|
+
await browser.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
void main().catch((error) => {
|
|
124
|
+
console.error(JSON.stringify({
|
|
125
|
+
ok: false,
|
|
126
|
+
marketplace,
|
|
127
|
+
error: error instanceof Error ? error.message : String(error),
|
|
128
|
+
}, null, 2));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadChromium } from './playwright-runtime.js';
|
|
2
|
+
function getChromiumChannel() {
|
|
3
|
+
const configuredChannel = process.env.PLAYWRIGHT_CHROMIUM_CHANNEL?.trim();
|
|
4
|
+
return configuredChannel && configuredChannel.length > 0 ? configuredChannel : undefined;
|
|
5
|
+
}
|
|
6
|
+
async function main() {
|
|
7
|
+
const chromium = await loadChromium();
|
|
8
|
+
const browser = await chromium.launch({
|
|
9
|
+
headless: true,
|
|
10
|
+
channel: getChromiumChannel(),
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
const page = await browser.newPage();
|
|
14
|
+
await page.goto('about:blank');
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
ok: true,
|
|
17
|
+
browserVersion: browser.version(),
|
|
18
|
+
pageUrl: page.url(),
|
|
19
|
+
}, null, 2));
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await browser.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
void main().catch((error) => {
|
|
26
|
+
console.error(JSON.stringify({
|
|
27
|
+
ok: false,
|
|
28
|
+
error: error instanceof Error ? error.message : String(error),
|
|
29
|
+
}, null, 2));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
@@ -1,29 +1,41 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import { resolve } from 'path';
|
|
2
3
|
import { fileURLToPath } from 'url';
|
|
3
4
|
import { spawn } from 'child_process';
|
|
4
5
|
function isHostedEnvironment() {
|
|
5
|
-
return (process.env.
|
|
6
|
+
return (process.env.DISABLE_DOTENVX === '1' ||
|
|
7
|
+
process.env.DISABLE_DOTENVX === 'true' ||
|
|
8
|
+
process.env.NIXPACKS === '1' ||
|
|
9
|
+
process.env.NIXPACKS_METADATA !== undefined ||
|
|
10
|
+
process.env.RENDER === 'true' ||
|
|
6
11
|
process.env.RAILWAY_ENVIRONMENT !== undefined ||
|
|
7
12
|
process.env.VERCEL === '1' ||
|
|
8
13
|
process.env.K_SERVICE !== undefined ||
|
|
9
14
|
process.env.AWS_EXECUTION_ENV !== undefined);
|
|
10
15
|
}
|
|
16
|
+
function hasLocalEnvFile() {
|
|
17
|
+
return existsSync(resolve(process.cwd(), '.env'));
|
|
18
|
+
}
|
|
11
19
|
function main() {
|
|
12
20
|
const args = process.argv.slice(2);
|
|
13
21
|
if (args.length === 0) {
|
|
14
22
|
throw new Error('Usage: tsx src/scripts/env-check.ts <command> [args...]');
|
|
15
23
|
}
|
|
16
24
|
const hosted = isHostedEnvironment();
|
|
25
|
+
const useDotenvx = !hosted && hasLocalEnvFile();
|
|
17
26
|
const [command, ...commandArgs] = args;
|
|
18
|
-
const finalCommand =
|
|
19
|
-
const finalArgs =
|
|
20
|
-
?
|
|
21
|
-
: // --overload ensures .env values always win over any pre-set shell env vars
|
|
27
|
+
const finalCommand = useDotenvx ? 'npx' : command;
|
|
28
|
+
const finalArgs = useDotenvx
|
|
29
|
+
? // --overload ensures .env values always win over any pre-set shell env vars
|
|
22
30
|
// (e.g. a stale EBAY_TOKEN_STORE_BACKEND exported in .zshrc from a previous run)
|
|
23
|
-
['-y', '@dotenvx/dotenvx', 'run', '--overload', '--', command, ...commandArgs]
|
|
24
|
-
|
|
31
|
+
['-y', '@dotenvx/dotenvx', 'run', '--overload', '--', command, ...commandArgs]
|
|
32
|
+
: commandArgs;
|
|
33
|
+
const modeMessage = hosted
|
|
25
34
|
? '[env-launcher] Hosted environment detected, using platform-provided env vars'
|
|
26
|
-
:
|
|
35
|
+
: useDotenvx
|
|
36
|
+
? '[env-launcher] Local .env detected, loading env via dotenvx'
|
|
37
|
+
: '[env-launcher] No local .env detected, running without dotenvx';
|
|
38
|
+
console.log(modeMessage);
|
|
27
39
|
const child = spawn(finalCommand, finalArgs, {
|
|
28
40
|
stdio: 'inherit',
|
|
29
41
|
shell: false,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { inspectEbayResearchSessionPersistence } from '../validation/providers/ebay-research.js';
|
|
2
|
+
function getMarketplace() {
|
|
3
|
+
const cliMarketplace = process.argv[2]?.trim();
|
|
4
|
+
if (cliMarketplace && cliMarketplace.length > 0) {
|
|
5
|
+
return cliMarketplace;
|
|
6
|
+
}
|
|
7
|
+
const configuredMarketplace = process.env.EBAY_RESEARCH_BOOTSTRAP_MARKETPLACE?.trim();
|
|
8
|
+
return configuredMarketplace && configuredMarketplace.length > 0
|
|
9
|
+
? configuredMarketplace
|
|
10
|
+
: 'EBAY-US';
|
|
11
|
+
}
|
|
12
|
+
async function main() {
|
|
13
|
+
const marketplace = getMarketplace();
|
|
14
|
+
const persistence = await inspectEbayResearchSessionPersistence(marketplace);
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
ok: persistence.error === null,
|
|
17
|
+
marketplace,
|
|
18
|
+
sessionStoreConfigured: persistence.sessionStoreConfigured,
|
|
19
|
+
sessionStoreSelected: persistence.sessionStoreSelected,
|
|
20
|
+
sessionStoreConfiguredFrom: persistence.sessionStoreConfiguredFrom,
|
|
21
|
+
sessionStoreRawConfiguredValue: persistence.sessionStoreRawConfiguredValue,
|
|
22
|
+
storeTargetConnection: persistence.storeTargetConnection,
|
|
23
|
+
storeCredentialsConfigured: persistence.storeCredentialsConfigured,
|
|
24
|
+
storeCredentialFingerprint: persistence.storeCredentialFingerprint,
|
|
25
|
+
researchEnvironment: persistence.researchEnvironment,
|
|
26
|
+
storageStateKeyScope: persistence.storageStateKeyScope,
|
|
27
|
+
canonical: {
|
|
28
|
+
storageStateKey: persistence.canonicalStateKey,
|
|
29
|
+
metadataKey: persistence.canonicalMetaKey,
|
|
30
|
+
storageStateExists: persistence.storageStateExists,
|
|
31
|
+
metadataExists: persistence.metadataExists,
|
|
32
|
+
storageStateBytes: persistence.storageStateBytes,
|
|
33
|
+
validPlaywrightStorageStateJson: persistence.storageStateValid,
|
|
34
|
+
},
|
|
35
|
+
freshCanonicalReadback: persistence.freshCanonicalReadback,
|
|
36
|
+
error: persistence.error,
|
|
37
|
+
}, null, 2));
|
|
38
|
+
if (persistence.error) {
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
void main().catch((error) => {
|
|
43
|
+
console.error(JSON.stringify({
|
|
44
|
+
ok: false,
|
|
45
|
+
marketplace: getMarketplace(),
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
}, null, 2));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|