commit 17442e053a27d29d6b7831140935ef2f8769f880 Author: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed Apr 29 19:44:46 2026 -0400 Initial commit: Coupa PO scraper and Payee Central site resolver Email MBOX parser, Coupa web scraper, and Payee Central scrapers for extracting PO data and resolving Amazon site codes. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44cf079 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +output/ +chrome-profile*/ +cookies.json +.DS_Store diff --git a/gen-po-list.mjs b/gen-po-list.mjs new file mode 100644 index 0000000..5e68b0f --- /dev/null +++ b/gen-po-list.mjs @@ -0,0 +1,53 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb"; +import { readFileSync, writeFileSync, mkdirSync } from "fs"; +import { execSync } from "child_process"; + +mkdirSync("output", { recursive: true }); + +// Get all PO numbers from DynamoDB +const client = new DynamoDBClient({ region: "us-east-1" }); +const docClient = DynamoDBDocumentClient.from(client); + +const dbPOs = new Set(); +let lastKey = undefined; + +console.log("Scanning DynamoDB for existing PO numbers..."); +while (true) { + const resp = await docClient.send( + new ScanCommand({ + TableName: "purchase-orders", + ProjectionExpression: "po_number", + ExclusiveStartKey: lastKey, + }) + ); + for (const item of resp.Items) { + dbPOs.add(item.po_number); + } + lastKey = resp.LastEvaluatedKey; + if (!lastKey) break; +} +console.log(`Found ${dbPOs.size} POs in DynamoDB`); + +// Get POs from invoice spreadsheet using python helper +console.log("Extracting POs from invoice spreadsheet..."); +const pyScript = [ + "import openpyxl, json", + 'wb = openpyxl.load_workbook("/Users/adammoussa/Documents/working-docs/plumbing-spend/invoices-2025.xlsx")', + "ws = wb.active", + "pos = set()", + "for row in ws.iter_rows(min_row=2, values_only=True):", + " if row[1]: pos.add(str(row[1]).strip())", + "print(json.dumps(sorted(list(pos))))", +].join("\n"); +const invoicePOs = JSON.parse( + execSync(`python3.12 -c "${pyScript.replace(/"/g, '\\"')}"`, { encoding: "utf-8" }) +); +console.log(`Found ${invoicePOs.length} unique POs in invoice file`); + +// Find POs not in DB +const notInDb = invoicePOs.filter((po) => !dbPOs.has(po)); +console.log(`POs not in DynamoDB: ${notInDb.length}`); + +writeFileSync("output/po-list.json", JSON.stringify(notInDb, null, 2)); +console.log("Saved to output/po-list.json"); diff --git a/monitor.mjs b/monitor.mjs new file mode 100644 index 0000000..4da7ee5 --- /dev/null +++ b/monitor.mjs @@ -0,0 +1,163 @@ +import { readFileSync, watchFile, existsSync } from "fs"; + +const OUTPUT_FILE = "output/scraped-pos.json"; +const PROGRESS_FILE = "output/progress.json"; +const POLL_MS = 5000; + +let lastCount = 0; + +function classify(description) { + if (!description) return null; + const d = description.toLowerCase(); + if (!d.includes("plumbing")) return null; + if (d.includes("plumbing pm") || d.includes("pm and corrective")) return "PM"; + return "Reactive"; +} + +function extractSite(record) { + if (record.site_code) return record.site_code; + for (const li of record.line_items || []) { + if (!li.description) continue; + const m = li.description.match(/^([A-Z]{1,4}[0-9]{1,2}|[A-Z]{4,5})\b/); + if (m) return m[1]; + } + if (record.ship_to_raw) { + const m = record.ship_to_raw.match(/\(([A-Z0-9]{3,5})\)/); + if (m) return m[1]; + } + return "Unknown"; +} + +function analyze() { + if (!existsSync(OUTPUT_FILE)) { + console.log("Waiting for scrape output..."); + return; + } + + let data; + try { + data = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8")); + } catch { + return; + } + + if (data.length === lastCount) return; + lastCount = data.length; + + let progress = {}; + if (existsSync(PROGRESS_FILE)) { + try { + progress = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")); + } catch {} + } + + const sites = {}; + let pmTotal = 0; + let reactiveTotal = 0; + let pmCount = 0; + let reactiveCount = 0; + let nonPlumbing = 0; + const plumbingPOs = []; + + for (const record of data) { + let isPlumbing = false; + let category = null; + + for (const li of record.line_items || []) { + const cat = classify(li.description); + if (cat) { + isPlumbing = true; + category = cat; + break; + } + } + + if (!isPlumbing) { + nonPlumbing++; + continue; + } + + const site = extractSite(record); + const amount = parseFloat( + (record.line_items[0]?.total || "0").replace(/,/g, "") + ); + + if (!sites[site]) sites[site] = { pm: 0, reactive: 0, pmCount: 0, reactiveCount: 0 }; + + if (category === "PM") { + sites[site].pm += amount; + sites[site].pmCount++; + pmTotal += amount; + pmCount++; + } else { + sites[site].reactive += amount; + sites[site].reactiveCount++; + reactiveTotal += amount; + reactiveCount++; + } + + plumbingPOs.push({ po: record.po_number, site, category, amount }); + } + + // Clear screen and print dashboard + console.clear(); + console.log("╔══════════════════════════════════════════════════════════════╗"); + console.log("║ COUPA PO SCRAPER — LIVE PLUMBING MONITOR ║"); + console.log("╚══════════════════════════════════════════════════════════════╝"); + console.log(); + console.log(` Scraped: ${data.length} / 4,192 POs (${Object.keys(progress).length} completed)`); + console.log(` Plumbing: ${pmCount + reactiveCount} POs | Non-plumbing: ${nonPlumbing}`); + console.log(); + console.log(" ┌──────────────┬──────────────────┬──────────────────┬──────────────────┐"); + console.log(" │ Category │ PO Count │ Spend │ Avg per PO │"); + console.log(" ├──────────────┼──────────────────┼──────────────────┼──────────────────┤"); + console.log( + ` │ PM │ ${String(pmCount).padStart(16)} │ $${pmTotal.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(pmCount ? pmTotal / pmCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │` + ); + console.log( + ` │ Reactive │ ${String(reactiveCount).padStart(16)} │ $${reactiveTotal.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(reactiveCount ? reactiveTotal / reactiveCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │` + ); + const total = pmTotal + reactiveTotal; + const totalCount = pmCount + reactiveCount; + console.log(" ├──────────────┼──────────────────┼──────────────────┼──────────────────┤"); + console.log( + ` │ TOTAL │ ${String(totalCount).padStart(16)} │ $${total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(totalCount ? total / totalCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │` + ); + console.log(" └──────────────┴──────────────────┴──────────────────┴──────────────────┘"); + + // Top sites + const sortedSites = Object.entries(sites) + .map(([site, d]) => ({ site, total: d.pm + d.reactive, ...d })) + .sort((a, b) => b.total - a.total); + + if (sortedSites.length > 0) { + console.log(); + console.log(" Top 15 Sites:"); + console.log(" ┌──────────┬──────────────────┬──────────────────┬──────────────────┐"); + console.log(" │ Site │ PM Spend │ Reactive Spend │ Total │"); + console.log(" ├──────────┼──────────────────┼──────────────────┼──────────────────┤"); + for (const s of sortedSites.slice(0, 15)) { + console.log( + ` │ ${s.site.padEnd(8)} │ $${s.pm.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${s.reactive.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${s.total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │` + ); + } + console.log(" └──────────┴──────────────────┴──────────────────┴──────────────────┘"); + } + + // Last 5 plumbing POs scraped + if (plumbingPOs.length > 0) { + console.log(); + console.log(" Last 5 plumbing POs:"); + for (const p of plumbingPOs.slice(-5)) { + console.log(` ${p.po} ${p.site.padEnd(8)} ${p.category.padEnd(10)} $${p.amount.toLocaleString()}`); + } + } + + console.log(); + console.log(` Last updated: ${new Date().toLocaleTimeString()}`); + console.log(" Press Ctrl+C to stop monitoring."); +} + +// Run immediately, then poll +analyze(); +setInterval(analyze, POLL_MS); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4b65103 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1492 @@ +{ + "name": "coupa-po-scraper", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "coupa-po-scraper", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.1039.0", + "@aws-sdk/lib-dynamodb": "^3.1039.0", + "playwright": "^1.59.1" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1039.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1039.0.tgz", + "integrity": "sha512-eqyrnSj2l0/y89LHENnGparEyKD33Wh8PQszgWbtb6t8bI5oriPaTHHXCdGUmFjOMkiNfqbsLCoo5HhMXlvbxw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/credential-provider-node": "^3.972.38", + "@aws-sdk/dynamodb-codec": "^3.973.7", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.11", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.37", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.23", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.7.tgz", + "integrity": "sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.33.tgz", + "integrity": "sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.35.tgz", + "integrity": "sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.37.tgz", + "integrity": "sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/credential-provider-env": "^3.972.33", + "@aws-sdk/credential-provider-http": "^3.972.35", + "@aws-sdk/credential-provider-login": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.33", + "@aws-sdk/credential-provider-sso": "^3.972.37", + "@aws-sdk/credential-provider-web-identity": "^3.972.37", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.37.tgz", + "integrity": "sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.38.tgz", + "integrity": "sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.33", + "@aws-sdk/credential-provider-http": "^3.972.35", + "@aws-sdk/credential-provider-ini": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.33", + "@aws-sdk/credential-provider-sso": "^3.972.37", + "@aws-sdk/credential-provider-web-identity": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.33.tgz", + "integrity": "sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.37.tgz", + "integrity": "sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/token-providers": "3.1039.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.37.tgz", + "integrity": "sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.7.tgz", + "integrity": "sha512-9NIOxillGcBp/355NoQo7kPalYIe8fJkgS00ZgO6qKLsiDmCgCoHGiT7yIuavWhsSbYcYIa8Z+BLdfnPtr5kSA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@smithy/core": "^3.23.17", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.5.tgz", + "integrity": "sha512-itVdge0NozgtgmtbZ25FVwWU3vGlE7x7feE/aOEJNkQfEpbkrF8Rj1QmnK+2blFfYE1xWt/iU+6/jUp/pv1+MA==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.1039.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.1039.0.tgz", + "integrity": "sha512-EDIi+6YBYFNaJcK5aXCDRnlIcDQWWJseii5vkwLFD2Rw89j2r179lQcPQZikaM2cv49vz4oaDyy1fdidlAlUyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/util-dynamodb": "^3.996.2", + "@smithy/core": "^3.23.17", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1039.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.11.tgz", + "integrity": "sha512-vXARCZVFQHdsd6qPPZyC/hh+5x2XsCYKqUQDCqnUlpGpChMpDojOOacQWdLJ+FFXKN8X3cmLOGrtgx/zysCKqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.36.tgz", + "integrity": "sha512-YhPix+0x/MdQrb1Ug1GDKeS5fqylIy+naz800asX8II4jqfTk2KY2KhmmYCwZcky8YWtRQQwWCGdoqeAnip8Uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.37.tgz", + "integrity": "sha512-N1oNpdiLoVAWYD3WFBnUi3LlfoDA06ZHo4ozyjbsJNLvILzvt//0CnR8N+CZ0NWeYgVB/5V59ivixHCWCx2ALw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.5.tgz", + "integrity": "sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.37", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.24", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.23", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.24.tgz", + "integrity": "sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.36", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1039.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1039.0.tgz", + "integrity": "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.996.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.2.tgz", + "integrity": "sha512-ddpwaZmjBzcApYN7lgtAXjk+u+GO8fiPsxzuc59UqP+zqdxI1gsenPvkyiHiF9LnYnyRGijz6oN2JylnN561qQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1003.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.23.tgz", + "integrity": "sha512-gGwq8L2Euw0aNG6Ey4EktiAo3fSCVoDy1CaBIthd+oeaKHPXUrNaApMewQ6La5Hv0lcznOtECZaNvYyc5LXXfA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..288a734 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "coupa-po-scraper", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.1039.0", + "@aws-sdk/lib-dynamodb": "^3.1039.0", + "playwright": "^1.59.1" + } +} diff --git a/parse-mbox.py b/parse-mbox.py new file mode 100644 index 0000000..26cdcb0 --- /dev/null +++ b/parse-mbox.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3.12 +"""Parse Coupa PO emails from an MBOX file and extract line items + ship-to data.""" + +import mailbox +import email +import re +import json +import sys +import os + +def parse_amount(s): + """Parse amount string, handling formats like '1.0 EACH x 3,000.00' or '10,000.00'.""" + s = s.strip() + if ' x ' in s.lower(): + s = s.split(' x ')[-1].strip() + elif ' X ' in s: + s = s.split(' X ')[-1].strip() + return float(s.replace(',', '')) + + +def parse_po_email(body): + """Extract PO data from a Coupa 'issued' email plaintext body.""" + lines = body.split('\n') + lines = [l.strip() for l in lines] + + # PO number from body + po_match = re.search(r'Purchase Order #?((?:2D|B187|FK)-\d+)', body) + if not po_match: + return None + po_number = po_match.group(1) + + # Line items: between "Items" line and the coupahost URL + items_start = None + items_end = None + for i, line in enumerate(lines): + if line == 'Items' and items_start is None: + items_start = i + 1 + if items_start and 'supplier.coupahost.com/orders/' in line: + items_end = i + break + + line_items = [] + if items_start and items_end: + item_lines = [l for l in lines[items_start:items_end] if l] + i = 0 + while i < len(item_lines): + desc = item_lines[i] + # Skip if this line looks like a number/currency + if re.match(r'^[\d,]', desc) or desc == 'USD': + i += 1 + continue + amount = None + # Look ahead for amount + for j in range(i + 1, min(i + 4, len(item_lines))): + candidate = item_lines[j] + if re.match(r'^[\d,]', candidate): + try: + amount = parse_amount(candidate) + except ValueError: + pass + break + line_items.append({ + 'description': desc, + 'amount': amount, + }) + # Skip past amount + currency lines + i = j + 1 if amount is not None else i + 1 + while i < len(item_lines) and item_lines[i] == 'USD': + i += 1 + + # Key-value pairs after "More Detail" + kv = {} + detail_start = None + for i, line in enumerate(lines): + if line == 'More Detail': + detail_start = i + 1 + break + + if detail_start: + kv_keys = ['PO ID', 'Department', 'Status', 'Last Opened', 'Order Date', + 'Acknowledged At', 'Revision Date', 'Payment Term', 'Req #'] + for i in range(detail_start, len(lines)): + for k in kv_keys: + if lines[i] == k and i + 1 < len(lines): + kv[k] = lines[i + 1] + + # Ship-to address: second "Shipping" section + shipping_count = 0 + ship_to_lines = [] + capturing = False + skipping_blanks = False + for i, line in enumerate(lines): + if line == 'Shipping': + shipping_count += 1 + if shipping_count == 2: + capturing = True + skipping_blanks = True + continue + elif capturing: + if skipping_blanks and line == '': + continue + skipping_blanks = False + if line in ('', 'Ship To Address') or line.startswith('---') or line.startswith('http'): + break + if line in kv_keys or line == 'Supplier' or line == 'More Detail': + break + ship_to_lines.append(line) + + ship_to_raw = '\n'.join(ship_to_lines) if ship_to_lines else None + + # Extract site code from ship-to + site_code = None + if ship_to_raw: + # (CODE) pattern — e.g. "Amazon.com Services LLC (XSF2)" + m = re.search(r'\(([A-Z0-9]{3,5})\)', ship_to_raw) + if m: + site_code = m.group(1) + # "LLC - CODE" — e.g. "Amazon.com Services LLC - WUT9" + if not site_code: + m = re.search(r'(?:LLC|Inc)\s*-\s*([A-Z0-9]{3,5})\b', ship_to_raw) + if m: + site_code = m.group(1) + # "ATTN: ... Station CODE" or "ATTN: ... DS - CODE" + if not site_code: + m = re.search(r'ATTN:.*?(?:Station|DS)\s*[-–]?\s*([A-Z0-9]{3,5})\b', ship_to_raw) + if m: + site_code = m.group(1) + # "CODE - Amazon" at start of first line + if not site_code: + m = re.match(r'^([A-Z0-9]{3,5})\s*-\s*Amazon', ship_to_raw) + if m: + site_code = m.group(1) + if not site_code and line_items: + for li in line_items: + m = re.match(r'^([A-Z]{1,4}[0-9]{1,2}|[A-Z]{4,5})\b', li['description']) + if m: + site_code = m.group(1) + break + + return { + 'po_number': po_number, + 'site_code': site_code, + 'status': kv.get('Status'), + 'order_date': kv.get('Order Date'), + 'revision_date': kv.get('Revision Date'), + 'payment_term': kv.get('Payment Term'), + 'req_number': kv.get('Req #'), + 'ship_to_raw': ship_to_raw, + 'line_items': line_items, + } + + +def process_mbox(mbox_path, target_pos=None, output_path=None): + """Process an MBOX file and return parsed PO records.""" + mbox = mailbox.mbox(mbox_path) + results = {} + processed = 0 + matched = 0 + + for msg in mbox: + processed += 1 + subject = msg.get('Subject', '') + + if 'issued' not in subject.lower() or 'Purchase Order' not in subject: + continue + + if msg.is_multipart(): + body = None + for part in msg.walk(): + if part.get_content_type() == 'text/plain': + body = part.get_payload(decode=True).decode('utf-8', errors='replace') + break + if not body: + continue + else: + body = msg.get_payload(decode=True).decode('utf-8', errors='replace') + + record = parse_po_email(body) + if not record: + continue + + po = record['po_number'] + + if target_pos and po not in target_pos: + continue + + # Keep latest version if duplicate + if po not in results: + results[po] = record + matched += 1 + + if matched % 500 == 0 and matched > 0: + print(f" Matched {matched} POs so far... ({processed} messages scanned)", file=sys.stderr) + + mbox.close() + + result_list = list(results.values()) + print(f"Processed {processed} messages, matched {matched} unique POs", file=sys.stderr) + + if output_path: + with open(output_path, 'w') as f: + json.dump(result_list, f, indent=2) + print(f"Saved to {output_path}", file=sys.stderr) + + return result_list + + +if __name__ == '__main__': + mbox_path = sys.argv[1] if len(sys.argv) > 1 else '/tmp/coupa-emails/coupa-po-dump--info@seahavenind.com-wHxmHD.mbox' + po_list_path = sys.argv[2] if len(sys.argv) > 2 else 'output/po-list.json' + output_path = sys.argv[3] if len(sys.argv) > 3 else 'output/email-parsed-pos.json' + + target_pos = None + if os.path.exists(po_list_path): + with open(po_list_path) as f: + target_pos = set(json.load(f)) + print(f"Targeting {len(target_pos)} specific POs", file=sys.stderr) + + process_mbox(mbox_path, target_pos, output_path) diff --git a/scrape-payee-chunk.mjs b/scrape-payee-chunk.mjs new file mode 100644 index 0000000..9e92114 --- /dev/null +++ b/scrape-payee-chunk.mjs @@ -0,0 +1,172 @@ +import { chromium } from "playwright"; +import { readFileSync, writeFileSync, existsSync } from "fs"; + +const chunkId = process.argv[2]; +if (!chunkId && chunkId !== "0") { + console.error("Usage: node scrape-payee-chunk.mjs "); + process.exit(1); +} + +const PO_LIST = `output/not-found-2026-chunk${chunkId}.json`; +const OUTPUT_FILE = `output/payee-2026-chunk${chunkId}-results.json`; +const PROGRESS_FILE = `output/payee-2026-chunk${chunkId}-progress.json`; +const PROFILE_DIR = `chrome-profile-${chunkId}`; +const DELAY_MS = 800; + +const allPOs = JSON.parse(readFileSync(PO_LIST, "utf-8")); +console.log(`[Chunk ${chunkId}] Loaded ${allPOs.length} POs`); + +let completed = {}; +if (existsSync(PROGRESS_FILE)) { + completed = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")); + console.log(`[Chunk ${chunkId}] Resuming: ${Object.keys(completed).length} already done`); +} + +const remaining = allPOs.filter((po) => !completed[po]); +console.log(`[Chunk ${chunkId}] Remaining: ${remaining.length} POs\n`); + +if (remaining.length === 0) { + console.log("All POs already scraped!"); + process.exit(0); +} + +function poUrl(po) { + return `https://payeecentral.amazon.com/PurchaseOrder/Details?poNumber=${po}`; +} + +// Copy chrome-profile if this chunk's profile doesn't exist +import { execSync } from "child_process"; +const profilePath = new URL(`./${PROFILE_DIR}`, import.meta.url).pathname; +const mainProfile = new URL("./chrome-profile", import.meta.url).pathname; +if (!existsSync(profilePath)) { + console.log(`Copying chrome profile to ${PROFILE_DIR}...`); + execSync(`cp -R "${mainProfile}" "${profilePath}"`); +} + +const context = await chromium.launchPersistentContext(profilePath, { + headless: false, + channel: "chrome", + args: ["--disable-blink-features=AutomationControlled"], + ignoreDefaultArgs: ["--enable-automation"], +}); + +const page = context.pages()[0] || (await context.newPage()); + +console.log(`[Chunk ${chunkId}] Opening Payee Central...`); +await page.goto(poUrl(remaining[0]), { waitUntil: "networkidle", timeout: 30000 }); + +if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log(`\n[Chunk ${chunkId}] 🔐 Login required. Please sign in...`); + await page.waitForURL( + (u) => { + const s = u.toString(); + return s.includes("payeecentral.amazon.com") && !s.includes("signin") && !s.includes("/ap/"); + }, + { timeout: 180000 } + ); + console.log(`[Chunk ${chunkId}] ✅ Logged in!\n`); +} + +let results = []; +if (existsSync(OUTPUT_FILE)) { + results = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8")); +} + +let scraped = 0; +let errors = 0; +let consecutiveErrors = 0; +const startTime = Date.now(); + +for (const po of remaining) { + try { + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + + if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log(`\n[Chunk ${chunkId}] 🔐 Session expired. Please log in again...`); + await page.waitForURL( + (u) => !u.toString().includes("signin") && !u.toString().includes("/ap/"), + { timeout: 180000 } + ); + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + } + + await page.waitForTimeout(500); + const bodyText = await page.innerText("body"); + + const shipToMatch = bodyText.match(/Ship To\s+([\s\S]*?)(?=Payee Central Account)/); + const shipTo = shipToMatch ? shipToMatch[1].trim() : null; + + let siteCode = null; + if (shipTo) { + for (const re of [ + /\(([A-Z0-9]{3,5})\)/, + /(?:LLC|Inc)\s*-\s*([A-Z0-9]{3,5})\b/, + /LLC\s+([A-Z0-9]{3,5})\b/, + /^([A-Z0-9]{3,5})\s*-\s*Amazon/m, + /Amazon\s+Fresh\s+([A-Z0-9]{3,5})\b/, + ]) { + const m = shipTo.match(re); + if (m) { siteCode = m[1]; break; } + } + } + + const lineItems = await page.$$eval( + "table tbody tr", + (rows) => + rows.map((row) => { + const cells = row.querySelectorAll("td"); + if (cells.length < 7) return null; + return { + description: cells[1]?.textContent?.trim() || null, + need_by: cells[2]?.textContent?.trim() || null, + quantity: cells[3]?.textContent?.trim() || null, + unit: cells[4]?.textContent?.trim() || null, + price: cells[5]?.textContent?.trim() || null, + amount: cells[6]?.textContent?.trim() || null, + }; + }).filter(Boolean) + ).catch(() => []); + + results.push({ + po_number: po, + site_code: siteCode, + ship_to_raw: shipTo, + line_items: lineItems, + }); + + completed[po] = true; + scraped++; + consecutiveErrors = 0; + + const elapsed = (Date.now() - startTime) / 1000; + const rate = scraped / elapsed; + const eta = Math.round((remaining.length - scraped) / rate); + const etaMin = Math.floor(eta / 60); + const etaSec = eta % 60; + + process.stdout.write( + `\r[C${chunkId}] [${scraped}/${remaining.length}] ${po} | ${siteCode || "?"} | ETA: ${etaMin}m${etaSec}s ` + ); + + if (scraped % 10 === 0) { + writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); + writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + } + } catch (err) { + console.error(`\n[C${chunkId}] [${po}] Error: ${err.message}`); + errors++; + consecutiveErrors++; + if (consecutiveErrors > 10) { + console.error("Too many consecutive errors, stopping."); + break; + } + } + + await page.waitForTimeout(DELAY_MS); +} + +writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); +writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + +console.log(`\n\n[Chunk ${chunkId}] Done! Scraped: ${scraped}, Errors: ${errors}`); +await context.close(); diff --git a/scrape-payee-notfound.mjs b/scrape-payee-notfound.mjs new file mode 100644 index 0000000..7fe79f8 --- /dev/null +++ b/scrape-payee-notfound.mjs @@ -0,0 +1,170 @@ +import { chromium } from "playwright"; +import { readFileSync, writeFileSync, existsSync } from "fs"; + +const PO_LIST = "output/not-found-pos.json"; +const OUTPUT_FILE = "output/notfound-payee-results.json"; +const PROGRESS_FILE = "output/notfound-payee-progress.json"; +const DELAY_MS = 800; + +const allPOs = JSON.parse(readFileSync(PO_LIST, "utf-8")); +console.log(`Loaded ${allPOs.length} POs to scrape from Payee Central`); + +let completed = {}; +if (existsSync(PROGRESS_FILE)) { + completed = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")); + console.log(`Resuming: ${Object.keys(completed).length} already scraped`); +} + +const remaining = allPOs.filter((po) => !completed[po]); +console.log(`Remaining: ${remaining.length} POs\n`); + +if (remaining.length === 0) { + console.log("All POs already scraped!"); + process.exit(0); +} + +function poUrl(po) { + return `https://payeecentral.amazon.com/PurchaseOrder/Details?poNumber=${po}`; +} + +const userDataDir = new URL("./chrome-profile", import.meta.url).pathname; +const context = await chromium.launchPersistentContext(userDataDir, { + headless: false, + channel: "chrome", + args: ["--disable-blink-features=AutomationControlled"], + ignoreDefaultArgs: ["--enable-automation"], +}); + +const page = context.pages()[0] || (await context.newPage()); + +console.log("Opening Payee Central..."); +await page.goto(poUrl(remaining[0]), { waitUntil: "networkidle", timeout: 30000 }); + +if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log("\n🔐 Login required. Please sign in in the browser window..."); + await page.waitForURL( + (u) => { + const s = u.toString(); + return s.includes("payeecentral.amazon.com") && !s.includes("signin") && !s.includes("/ap/"); + }, + { timeout: 180000 } + ); + console.log("✅ Logged in!\n"); +} + +let results = []; +if (existsSync(OUTPUT_FILE)) { + results = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8")); +} + +let scraped = 0; +let errors = 0; +let consecutiveErrors = 0; +const startTime = Date.now(); + +for (const po of remaining) { + try { + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + + if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log("\n🔐 Session expired. Please log in again..."); + await page.waitForURL( + (u) => !u.toString().includes("signin") && !u.toString().includes("/ap/"), + { timeout: 180000 } + ); + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + } + + await page.waitForTimeout(500); + const bodyText = await page.innerText("body"); + + const shipToMatch = bodyText.match(/Ship To\s+([\s\S]*?)(?=Payee Central Account)/); + const shipTo = shipToMatch ? shipToMatch[1].trim() : null; + + let siteCode = null; + if (shipTo) { + const m = shipTo.match(/\(([A-Z0-9]{3,5})\)/); + if (m) siteCode = m[1]; + if (!siteCode) { + const m2 = shipTo.match(/(?:LLC|Inc)\s*-\s*([A-Z0-9]{3,5})\b/); + if (m2) siteCode = m2[1]; + } + if (!siteCode) { + const m3 = shipTo.match(/LLC\s+([A-Z0-9]{3,5})\b/); + if (m3) siteCode = m3[1]; + } + if (!siteCode) { + const m4 = shipTo.match(/^([A-Z0-9]{3,5})\s*-\s*Amazon/m); + if (m4) siteCode = m4[1]; + } + if (!siteCode) { + const m5 = shipTo.match(/Amazon\s+Fresh\s+([A-Z0-9]{3,5})\b/); + if (m5) siteCode = m5[1]; + } + } + + const lineItems = await page.$$eval( + "table tbody tr", + (rows) => + rows.map((row) => { + const cells = row.querySelectorAll("td"); + if (cells.length < 7) return null; + return { + description: cells[1]?.textContent?.trim() || null, + need_by: cells[2]?.textContent?.trim() || null, + quantity: cells[3]?.textContent?.trim() || null, + unit: cells[4]?.textContent?.trim() || null, + price: cells[5]?.textContent?.trim() || null, + amount: cells[6]?.textContent?.trim() || null, + }; + }).filter(Boolean) + ).catch(() => []); + + const result = { + po_number: po, + site_code: siteCode, + ship_to_raw: shipTo, + line_items: lineItems, + }; + + results.push(result); + completed[po] = true; + scraped++; + consecutiveErrors = 0; + + const elapsed = (Date.now() - startTime) / 1000; + const rate = scraped / elapsed; + const eta = Math.round((remaining.length - scraped) / rate); + const etaMin = Math.floor(eta / 60); + const etaSec = eta % 60; + + const desc0 = lineItems[0]?.description?.substring(0, 40) || "no items"; + process.stdout.write( + `\r[${scraped}/${remaining.length}] ${po} | ${siteCode || "?"} | ${desc0} | ETA: ${etaMin}m${etaSec}s ` + ); + + if (scraped % 10 === 0) { + writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); + writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + } + } catch (err) { + console.error(`\n [${po}] Error: ${err.message}`); + errors++; + consecutiveErrors++; + if (consecutiveErrors > 10) { + console.error("Too many consecutive errors, stopping."); + break; + } + } + + await page.waitForTimeout(DELAY_MS); +} + +writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); +writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + +console.log(`\n\nDone! Scraped: ${scraped}, Errors: ${errors}`); +console.log(`Total in output: ${results.length} POs`); +console.log(`Results saved to ${OUTPUT_FILE}`); + +await context.close(); diff --git a/scrape-payee.mjs b/scrape-payee.mjs new file mode 100644 index 0000000..c0b47c8 --- /dev/null +++ b/scrape-payee.mjs @@ -0,0 +1,159 @@ +import { chromium } from "playwright"; +import { readFileSync, writeFileSync, existsSync } from "fs"; + +const PO_LIST = "output/unknown-pos.json"; +const OUTPUT_FILE = "output/payee-results.json"; +const PROGRESS_FILE = "output/payee-progress.json"; +const DELAY_MS = 800; + +const allPOs = JSON.parse(readFileSync(PO_LIST, "utf-8")); +console.log(`Loaded ${allPOs.length} POs to scrape from Payee Central`); + +let completed = {}; +if (existsSync(PROGRESS_FILE)) { + completed = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")); + console.log(`Resuming: ${Object.keys(completed).length} already scraped`); +} + +const remaining = allPOs.filter((po) => !completed[po]); +console.log(`Remaining: ${remaining.length} POs\n`); + +if (remaining.length === 0) { + console.log("All POs already scraped!"); + process.exit(0); +} + +function poUrl(po) { + return `https://payeecentral.amazon.com/PurchaseOrder/Details?poNumber=${po}`; +} + +const userDataDir = new URL("./chrome-profile", import.meta.url).pathname; +const context = await chromium.launchPersistentContext(userDataDir, { + headless: false, + channel: "chrome", + args: ["--disable-blink-features=AutomationControlled"], + ignoreDefaultArgs: ["--enable-automation"], +}); + +const page = context.pages()[0] || (await context.newPage()); + +console.log("Opening Payee Central..."); +await page.goto(poUrl(remaining[0]), { waitUntil: "networkidle", timeout: 30000 }); + +if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log("\n🔐 Login required. Please sign in in the browser window..."); + await page.waitForURL( + (u) => { + const s = u.toString(); + return s.includes("payeecentral.amazon.com") && !s.includes("signin") && !s.includes("/ap/"); + }, + { timeout: 180000 } + ); + console.log("✅ Logged in!\n"); +} + +let results = []; +if (existsSync(OUTPUT_FILE)) { + results = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8")); +} + +let scraped = 0; +let errors = 0; +let consecutiveErrors = 0; +const startTime = Date.now(); + +for (const po of remaining) { + try { + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + + if (page.url().includes("signin") || page.url().includes("/ap/")) { + console.log("\n🔐 Session expired. Please log in again..."); + await page.waitForURL( + (u) => !u.toString().includes("signin") && !u.toString().includes("/ap/"), + { timeout: 180000 } + ); + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 20000 }); + } + + await page.waitForTimeout(500); + const bodyText = await page.innerText("body"); + + // Extract Ship To with site code + const shipToMatch = bodyText.match(/Ship To\s+([\s\S]*?)(?=Payee Central Account)/); + const shipTo = shipToMatch ? shipToMatch[1].trim() : null; + + let siteCode = null; + if (shipTo) { + const m = shipTo.match(/\(([A-Z0-9]{3,5})\)/); + if (m) siteCode = m[1]; + if (!siteCode) { + const m2 = shipTo.match(/(?:LLC|Inc)\s*-\s*([A-Z0-9]{3,5})\b/); + if (m2) siteCode = m2[1]; + } + } + + // Extract line items from table + const lineItems = await page.$$eval( + "table tbody tr", + (rows) => + rows.map((row) => { + const cells = row.querySelectorAll("td"); + if (cells.length < 7) return null; + return { + description: cells[1]?.textContent?.trim() || null, + need_by: cells[2]?.textContent?.trim() || null, + quantity: cells[3]?.textContent?.trim() || null, + unit: cells[4]?.textContent?.trim() || null, + price: cells[5]?.textContent?.trim() || null, + amount: cells[6]?.textContent?.trim() || null, + }; + }).filter(Boolean) + ).catch(() => []); + + const result = { + po_number: po, + site_code: siteCode, + ship_to_raw: shipTo, + line_items: lineItems, + }; + + results.push(result); + completed[po] = true; + scraped++; + consecutiveErrors = 0; + + const elapsed = (Date.now() - startTime) / 1000; + const rate = scraped / elapsed; + const eta = Math.round((remaining.length - scraped) / rate); + const etaMin = Math.floor(eta / 60); + const etaSec = eta % 60; + + process.stdout.write( + `\r[${scraped}/${remaining.length}] ${po} | ${siteCode || "?"} | ETA: ${etaMin}m${etaSec}s ` + ); + + if (scraped % 10 === 0) { + writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); + writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + } + } catch (err) { + console.error(`\n [${po}] Error: ${err.message}`); + errors++; + consecutiveErrors++; + if (consecutiveErrors > 10) { + console.error("Too many consecutive errors, stopping."); + break; + } + } + + await page.waitForTimeout(DELAY_MS); +} + +writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); +writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + +console.log(`\n\nDone! Scraped: ${scraped}, Errors: ${errors}`); +console.log(`Total in output: ${results.length} POs`); +console.log(`Results saved to ${OUTPUT_FILE}`); + +await context.close(); diff --git a/scrape.mjs b/scrape.mjs new file mode 100644 index 0000000..673f24a --- /dev/null +++ b/scrape.mjs @@ -0,0 +1,292 @@ +import { chromium } from "playwright"; +import { readFileSync, writeFileSync, existsSync } from "fs"; + +const EMAIL = process.env.COUPA_EMAIL; +const PASSWORD = process.env.COUPA_PASSWORD; + +if (!EMAIL || !PASSWORD) { + console.error("Set COUPA_EMAIL and COUPA_PASSWORD env vars"); + process.exit(1); +} + +const SUPPLIER_ID = "895025"; +const INSTANCE_URL = "https%3A%2F%2Famazon.coupahost.com%2F"; +const OUTPUT_FILE = "output/scraped-pos.json"; +const PROGRESS_FILE = "output/progress.json"; +const DELAY_MS = 1500; +const KEEPALIVE_EVERY = 75; + +const poListFile = process.argv[2]; +if (!poListFile) { + console.error("Usage: node scrape.mjs "); + process.exit(1); +} + +const allPOs = JSON.parse(readFileSync(poListFile, "utf-8")); +console.log(`Loaded ${allPOs.length} POs to scrape`); + +let completed = {}; +if (existsSync(PROGRESS_FILE)) { + completed = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")); + console.log(`Resuming: ${Object.keys(completed).length} already scraped`); +} + +const remaining = allPOs.filter((po) => !completed[po]); +console.log(`Remaining: ${remaining.length} POs\n`); + +if (remaining.length === 0) { + console.log("All POs already scraped!"); + process.exit(0); +} + +function poUrl(po) { + const numeric = po.split("-").slice(1).join("-"); + return `https://supplier.coupahost.com/orders/${numeric}?instance_url=${INSTANCE_URL}&supplier_id=${SUPPLIER_ID}&`; +} + +function isLoginPage(url) { + return url.includes("sessions/new") || url.includes("login") || url.includes("sign_in"); +} + +async function autoLogin(page) { + console.log("\n🔐 Session expired — auto-filling login form..."); + + // Step 1: Fill email and click Continue + try { + await page.waitForSelector("input#email", { state: "visible", timeout: 5000 }); + await page.fill("input#email", EMAIL); + await page.click("button.s-login").catch(() => {}); + console.log(" Email filled, clicked Continue."); + } catch { + // Already past email step + } + + // Step 2: CAPTCHA appears before password — wait for user to solve it + // After CAPTCHA, password field will appear + console.log(" ⏳ Solve the CAPTCHA — password will auto-fill after..."); + try { + await page.waitForSelector("input#password", { state: "visible", timeout: 120000 }); + await page.fill("input#password", PASSWORD); + console.log(" Password filled, clicking Login..."); + page.click("button.s-login").catch(() => {}); + } catch { + console.log(" Password field never appeared. Check the browser."); + } + + // Wait for login to complete (may need another CAPTCHA or just redirects) + console.log(" ⏳ Waiting for login to complete..."); + try { + await page.waitForURL((url) => !isLoginPage(url.toString()), { timeout: 120000 }); + console.log(" ✅ Login successful!\n"); + return true; + } catch { + console.error(" ❌ Login timed out after 120s. Exiting."); + return false; + } +} + +async function keepAlive(page) { + try { + await page.goto("https://supplier.coupahost.com/home/", { + waitUntil: "domcontentloaded", + timeout: 15000, + }); + if (isLoginPage(page.url())) return false; + return true; + } catch { + return false; + } +} + +async function scrapePO(page, po) { + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 30000 }); + + if (isLoginPage(page.url())) { + const ok = await autoLogin(page); + if (!ok) return null; + // Navigate to PO after re-login + await page.goto(poUrl(po), { waitUntil: "networkidle", timeout: 30000 }); + if (isLoginPage(page.url())) return null; + } + + await page.waitForSelector("iframe#enterprise_frame", { timeout: 15000 }); + await page.waitForTimeout(1500); + + const frame = + page.frame({ name: "enterprise_frame" }) || + page.frames().find((f) => f.url().includes("supplier_order_headers")); + + if (!frame) return "no_iframe"; + + await frame.waitForLoadState("networkidle"); + + const data = await frame.evaluate(() => { + const text = (sel) => { + const el = document.querySelector(sel); + return el ? el.textContent.trim() : null; + }; + + const lineItems = []; + const rows = document.querySelectorAll(".coupa_datatable_row"); + for (const row of rows) { + const desc = row.querySelector(".s-description"); + const price = row.querySelector(".s-price span[title]"); + const total = row.querySelector(".s-total span[title]"); + const needBy = row.querySelector("[id$='_need_by_date']"); + lineItems.push({ + description: desc ? desc.textContent.trim() : null, + price: price ? price.textContent.trim() : null, + total: total ? total.textContent.trim() : null, + need_by: needBy ? needBy.textContent.trim() : null, + }); + } + + const addrEl = document.querySelector( + "label[for='order_header_ship_to_address'] + span.address, span.address" + ); + let shipTo = null; + if (addrEl) { + const clone = addrEl.cloneNode(true); + clone.querySelectorAll(".form_element").forEach((el) => el.remove()); + shipTo = clone.innerHTML + .replace(//gi, "\n") + .replace(/<[^>]+>/g, "") + .trim(); + } + + return { + status: text(".s-readable_status"), + order_date: text(".s-local_header_created_at"), + requester: text(".s-requester"), + payment_term: text("#order_header_payment_term .data"), + ship_to_raw: shipTo, + line_items: lineItems, + }; + }); + + let site_code = null; + if (data.ship_to_raw) { + const m = data.ship_to_raw.match(/\(([A-Z0-9]{3,5})\)/); + if (m) site_code = m[1]; + if (!site_code) { + const m2 = data.ship_to_raw.match(/Attn:\s*([A-Z0-9]{3,5})\b/); + if (m2) site_code = m2[1]; + } + } + + return { po_number: po, site_code, ...data }; +} + +// --- Main --- +// Use a persistent Chrome profile so reCAPTCHA trusts the browser +const userDataDir = new URL("./chrome-profile", import.meta.url).pathname; +const context = await chromium.launchPersistentContext(userDataDir, { + headless: false, + channel: "chrome", + args: ["--disable-blink-features=AutomationControlled"], + ignoreDefaultArgs: ["--enable-automation"], +}); +const page = context.pages()[0] || (await context.newPage()); + +// Initial login +console.log("Opening Coupa supplier portal..."); +await page.goto(poUrl(remaining[0]), { waitUntil: "networkidle", timeout: 30000 }); + +if (isLoginPage(page.url())) { + const ok = await autoLogin(page); + if (!ok) { + await context.close(); + process.exit(1); + } +} + +let results = []; +if (existsSync(OUTPUT_FILE)) { + results = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8")); +} + +let scraped = 0; +let errors = 0; +let consecutiveErrors = 0; +const startTime = Date.now(); + +for (const po of remaining) { + // Session keep-alive + if (scraped > 0 && scraped % KEEPALIVE_EVERY === 0) { + process.stdout.write("\n Refreshing session..."); + const alive = await keepAlive(page); + if (!alive) { + console.log(" session expired, re-logging in..."); + await page.goto("https://supplier.coupahost.com/sessions/new", { + waitUntil: "networkidle", + }); + const ok = await autoLogin(page); + if (!ok) break; + } else { + process.stdout.write(" ok\n"); + } + } + + try { + const result = await scrapePO(page, po); + + if (result === null) { + console.error(`\nFailed to authenticate. Saving progress.`); + break; + } + + if (result === "no_iframe") { + console.error(`\n [${po}] No iframe found, skipping`); + errors++; + consecutiveErrors++; + if (consecutiveErrors > 10) { + console.error("Too many consecutive errors, stopping."); + break; + } + continue; + } + + results.push(result); + completed[po] = true; + scraped++; + consecutiveErrors = 0; + + const elapsed = (Date.now() - startTime) / 1000; + const rate = scraped / elapsed; + const eta = Math.round((remaining.length - scraped) / rate); + const etaMin = Math.floor(eta / 60); + const etaSec = eta % 60; + const desc0 = result.line_items[0]?.description?.substring(0, 50) || "no lines"; + + process.stdout.write( + `\r[${scraped}/${remaining.length}] ${po} | ${result.site_code || "?"} | ${desc0}... | ETA: ${etaMin}m${etaSec}s ` + ); + + // Save every 10 POs (more frequent for monitor) + if (scraped % 10 === 0) { + writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); + writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + } + } catch (err) { + console.error(`\n [${po}] Error: ${err.message}`); + errors++; + consecutiveErrors++; + + if (consecutiveErrors > 10) { + console.error("Too many consecutive errors, stopping."); + break; + } + } + + await page.waitForTimeout(DELAY_MS); +} + +// Final save +writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); +writeFileSync(PROGRESS_FILE, JSON.stringify(completed)); + +console.log(`\n\nDone! Scraped: ${scraped}, Errors: ${errors}`); +console.log(`Total in output: ${results.length} POs`); +console.log(`Results saved to ${OUTPUT_FILE}`); + +await context.close();