From 917930d0fa29437ee8a00766c41630b7e58e2b2b Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 6 Jul 2026 01:31:11 +0200 Subject: [PATCH] Genesis --- .env.example | 34 + .gitignore | 4 + .npmrc | 1 + generate-cert.js | 24 + package-lock.json | 1542 +++++++++++++++++++++++++++++ package.json | 23 + server.js | 124 +++ src/db.js | 49 + src/endpoints/category.js | 23 + src/endpoints/cimage.js | 6 + src/endpoints/client.js | 71 ++ src/endpoints/customergroup.js | 13 + src/endpoints/image-handler.js | 21 + src/endpoints/index.js | 10 + src/endpoints/init.js | 33 + src/endpoints/order.js | 48 + src/endpoints/pimage.js | 6 + src/endpoints/product.js | 67 ++ src/http.js | 35 + src/image-resize.js | 9 + src/jtl-server.js | 52 + src/logger.js | 20 + src/order-log.js | 28 + src/pairing.js | 24 + src/queries/category-count.js | 18 + src/queries/category-list.js | 48 + src/queries/category-tree.js | 18 + src/queries/customer-groups.js | 48 + src/queries/image.js | 43 + src/queries/product-attributes.js | 149 +++ src/queries/product-count.js | 20 + src/queries/product-list.js | 151 +++ src/request-log.js | 20 + test-client.js | 130 +++ 34 files changed, 2912 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 generate-cert.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js create mode 100644 src/db.js create mode 100644 src/endpoints/category.js create mode 100644 src/endpoints/cimage.js create mode 100644 src/endpoints/client.js create mode 100644 src/endpoints/customergroup.js create mode 100644 src/endpoints/image-handler.js create mode 100644 src/endpoints/index.js create mode 100644 src/endpoints/init.js create mode 100644 src/endpoints/order.js create mode 100644 src/endpoints/pimage.js create mode 100644 src/endpoints/product.js create mode 100644 src/http.js create mode 100644 src/image-resize.js create mode 100644 src/jtl-server.js create mode 100644 src/logger.js create mode 100644 src/order-log.js create mode 100644 src/pairing.js create mode 100644 src/queries/category-count.js create mode 100644 src/queries/category-list.js create mode 100644 src/queries/category-tree.js create mode 100644 src/queries/customer-groups.js create mode 100644 src/queries/image.js create mode 100644 src/queries/product-attributes.js create mode 100644 src/queries/product-count.js create mode 100644 src/queries/product-list.js create mode 100644 src/request-log.js create mode 100644 test-client.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9d406d1 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# HTTPS POS server +PORT=4443 +AUTH_TOKEN=df40ad2067954646abb0499548a52241 +PAIRING_CODE=307018 +LOG_FILE=logs/requests.log +ORDER_LOG_FILE=logs/orders.log + +# TLS certificate metadata returned during pairing +CERTIFICATE_FINGERPRINT=BC2114CF407A42724BEEF417960F76DCBF9DE879 +CERTIFICATE_SERIAL_NUMBER=00BFC8BEACDB981B165210EF111CB9D3 +SERVER_FINGERPRINT=39-6D-BD-DE-F3-5C-5A-EA-C2-19-CF-EB-A7-A9-58-2F-20-3F-20-F7-3D-E6-CA-8E-AE-FD-28-30-37-A6-45-AE + +# Mandant +MANDANT_ID=1 +MANDANT_NAME=eB-Standard +MANDANT_DATABASE=eazybusiness +ROOT_CATEGORY_ID=1 + +# Category sync (tKategorieSprache / tKategoriebildPlattform lookups) +LANGUAGE_ID=1 +IMAGE_PLATFORM_ID=1 +IMAGE_SHOP_ID=0 + +# Product sync (tSteuerzone.cName used to look up tax rates per tSteuerklasse) +TAX_ZONE_NAME=Zone-EU + +# MSSQL (connection data for JTL-Wawi database) +MSSQL_SERVER=localhost +MSSQL_PORT=1433 +MSSQL_DATABASE=eazybusiness +MSSQL_USER=sa +MSSQL_PASSWORD= +MSSQL_ENCRYPT=false +MSSQL_TRUST_SERVER_CERTIFICATE=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f7f36b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +certs/ +logs/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..2ae2317 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +loglevel=silent diff --git a/generate-cert.js b/generate-cert.js new file mode 100644 index 0000000..3a4eafc --- /dev/null +++ b/generate-cert.js @@ -0,0 +1,24 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { logger } from './src/logger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const certsDir = path.join(__dirname, 'certs'); +const keyPath = path.join(certsDir, 'key.pem'); +const certPath = path.join(certsDir, 'cert.pem'); + +fs.mkdirSync(certsDir, { recursive: true }); + +const subject = '/CN=localhost/O=JTL POS Sync/C=DE'; +const san = 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0'; + +execSync( + `openssl req -x509 -newkey rsa:2048 -nodes -keyout "${keyPath}" -out "${certPath}" -days 3650 -subj "${subject}" -addext "${san}"`, + { stdio: 'inherit' } +); + +logger.success(`Wrote ${keyPath}`); +logger.success(`Wrote ${certPath}`); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8d3dcf0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1542 @@ +{ + "name": "posSync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "posSync", + "version": "1.0.0", + "license": "0BSD", + "dependencies": { + "chalk": "^5.6.2", + "dotenv": "^17.4.2", + "mssql": "^12.7.0", + "sharp": "^0.35.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.7.0.tgz", + "integrity": "sha512-rL0lJqh1E8HLXNgjIw8cRyGAV/v+m6p1xRu/8OhsnmN8XHhwkyYJkAoGM+zrew96v7jZYPmVfy7pv7v4Iccfsg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.2.tgz", + "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", + "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz", + "integrity": "sha512-VmUSLbXRAbSzDD8grXHGPaknYs0SKr3yuf6U+d4XMpX4XuVYskNqbTTwXce0zR1LyxfTZm9rWEBcvs3vdYwCmQ==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.16.0.tgz", + "integrity": "sha512-Wc75FGnQgYpsm5jsOqn1H8AXsh8vXruA6vwip1nhjrJxwby7juxKAIVLr7csepmHiwdZGr6EwI5BlSc3PizEtQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.0.tgz", + "integrity": "sha512-UikJOtMwkFpZNzTH6Dqk8UTUPbow15zH3e0UjGYZy69lYENW/S05gMLhbxI2eonz66uALhIljvhsSMEb6+O30g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.3.1.tgz", + "integrity": "sha512-sqqv3L1UOI4KDXonNtbxPYUgbSWVXqxvmmb6BUw9n4P/UXgG+cVur3dLWQN4Cz7qQ+UJROCCxMXlksm7gIq0Sw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@js-joda/core": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.0.1.tgz", + "integrity": "sha512-Iev3Cfa3MC+06JXXeFzSLew/7yeCXLMhjesyH/BD4EQYlUUDV49Ln3s8hd7p5XK5UesO/dO3K1LFJ7RT7v0Ddg==", + "license": "BSD-3-Clause" + }, + "node_modules/@tediousjs/connection-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-1.1.0.tgz", + "integrity": "sha512-z9ZBWEG+8pIB5V1zYzlRPXx0oRJ5H7coPnMQK8EZOw03UTPI9Umn6viL36f5w+CuqkKsnCM50RVStpjZmR0Bng==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", + "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mssql": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.0.tgz", + "integrity": "sha512-J6SJKXi1jYbhHjjooLNtPnX7+s3cq5IJ701Wgy/UW1SXRpgFlJJsYi3IPve9RVgCUkq0Cqv2aaaSJ4IXtIF3mg==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^1.0.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "tarn": "^3.0.2", + "tedious": "^19.2.2 || ^20.0.0" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18.19.0" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tarn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.1.0.tgz", + "integrity": "sha512-QDihlHbXxQ4SnuQcRd1TPNBHUYo/hafDRDP82COYSVfRcx/3qnfGDkn1WFjozVl+AYr8ZyDKSQ/kIgHH1ri1yg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-20.0.0.tgz", + "integrity": "sha512-bTR0aou0Ghucf0ytvZUJjnKHGKDV8tT57jPYtEkSpfTWFe++4uR1wxJLQ4mh5wlSvAYXzmgBxbI0vaE56qigXw==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.10.1", + "@azure/identity": "^4.13.1", + "@azure/keyvault-keys": "^4.10.2", + "@js-joda/core": "^6.0.1", + "@types/node": ">=22", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=22" + } + }, + "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" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c05bec3 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "posSync", + "version": "1.0.0", + "description": "sync server", + "license": "0BSD", + "author": "", + "type": "module", + "main": "server.js", + "scripts": { + "cert": "node generate-cert.js", + "start": "node server.js", + "test:client": "node test-client.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "chalk": "^5.6.2", + "dotenv": "^17.4.2", + "mssql": "^12.7.0", + "sharp": "^0.35.3" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..a839c85 --- /dev/null +++ b/server.js @@ -0,0 +1,124 @@ +import fs from 'node:fs'; +import https from 'node:https'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import 'dotenv/config'; + +import { connectDb, closeDb } from './src/db.js'; +import { createJtlPosServer } from './src/jtl-server.js'; +import { createPairingStore } from './src/pairing.js'; +import { closeOrderLog } from './src/order-log.js'; +import { closeRequestLog, logRequest } from './src/request-log.js'; +import { logger } from './src/logger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const PORT = Number(process.env.PORT) || 4443; +const AUTH_TOKEN = process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241'; +const PAIRING_CODE = process.env.PAIRING_CODE || '307018'; + +const certsDir = path.join(__dirname, 'certs'); +const keyPath = path.join(certsDir, 'key.pem'); +const certPath = path.join(certsDir, 'cert.pem'); + +if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) { + logger.error('Missing TLS certificate. Run: npm run cert'); + process.exit(1); +} + +const CONSOLE_URL_MAX_LENGTH = 100; + +function truncateUrl(url) { + if (url.length <= CONSOLE_URL_MAX_LENGTH) { + return url; + } + return `${url.slice(0, CONSOLE_URL_MAX_LENGTH)}...`; +} + +function formatBody(buffer) { + if (!buffer.length) { + return '(empty)'; + } + const text = buffer.toString('utf8'); + if (/^[\x09\x0A\x0D\x20-\x7E\u0080-\uFFFF]*$/.test(text)) { + return text; + } + return `[binary ${buffer.length} bytes]`; +} + +const pairingStore = createPairingStore(); +pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS'); +pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS'); + +const jtlHandler = createJtlPosServer(pairingStore, { authToken: AUTH_TOKEN }); + +const loggedJtlHandler = async (req, res) => { + const started = Date.now(); + let responseBuffer = Buffer.alloc(0); + + const originalEnd = res.end.bind(res); + res.end = (chunk, ...args) => { + if (chunk) { + responseBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + } + return originalEnd(chunk, ...args); + }; + + await jtlHandler(req, res); + + const responseBody = formatBody(responseBuffer); + const durationMs = Date.now() - started; + logger.info(`${req.socket.remoteAddress} ${req.method} ${truncateUrl(req.url)} ${res.statusCode} ${durationMs}ms`); + + logRequest({ + remoteAddress: req.socket.remoteAddress, + method: req.method, + url: req.url, + statusCode: res.statusCode, + durationMs, + response: responseBody, + }); +}; + +const httpsServer = https.createServer( + { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), + }, + loggedJtlHandler +); + +async function start() { + try { + await connectDb(); + logger.success(`MSSQL connected: ${process.env.MSSQL_SERVER}/${process.env.MSSQL_DATABASE}`); + } catch (err) { + logger.warn(`MSSQL connection skipped: ${err.message}`); + logger.warn('POS handshake will still work; sync from database is not available yet.'); + } + + httpsServer.listen(PORT, '0.0.0.0', () => { + logger.success(`HTTPS POS server listening on https://0.0.0.0:${PORT}`); + logger.info(`Certificate: ${certPath}`); + logger.info(`Pairing code: ${PAIRING_CODE}`); + logger.info(`Auth token: ${AUTH_TOKEN}`); + logger.info('Import cert.pem into JTL POS trust store if required.'); + }); +} + +async function shutdown() { + logger.info('Shutting down...'); + httpsServer.close(); + await closeDb(); + closeRequestLog(); + closeOrderLog(); +} + +process.on('SIGINT', () => { + shutdown().finally(() => process.exit(0)); +}); +process.on('SIGTERM', () => { + shutdown().finally(() => process.exit(0)); +}); + +start(); diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..e513a29 --- /dev/null +++ b/src/db.js @@ -0,0 +1,49 @@ +import sql from 'mssql'; +import 'dotenv/config'; + +const config = { + server: process.env.MSSQL_SERVER || 'localhost', + port: Number(process.env.MSSQL_PORT) || 1433, + database: process.env.MSSQL_DATABASE || 'eazybusiness', + user: process.env.MSSQL_USER, + password: process.env.MSSQL_PASSWORD, + options: { + encrypt: process.env.MSSQL_ENCRYPT !== 'false', + trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE !== 'false', + }, +}; + +/** @type {sql.ConnectionPool | null} */ +let pool = null; + +export function getPool() { + if (!pool) { + throw new Error('MSSQL pool is not connected. Call connectDb() first.'); + } + return pool; +} + +export async function connectDb() { + if (pool) { + return pool; + } + + if (!config.user) { + throw new Error('MSSQL_USER is not set in environment'); + } + + pool = await sql.connect(config); + return pool; +} + +export async function closeDb() { + if (pool) { + await pool.close(); + pool = null; + } +} + +export async function pingDb() { + const result = await getPool().request().query('SELECT 1 AS ok'); + return result.recordset[0]?.ok === 1; +} diff --git a/src/endpoints/category.js b/src/endpoints/category.js new file mode 100644 index 0000000..776bc18 --- /dev/null +++ b/src/endpoints/category.js @@ -0,0 +1,23 @@ +import { sendJson, serverTimestamp } from '../http.js'; +import { getCategoryList } from '../queries/category-list.js'; + +export const method = 'GET'; +export const path = '/v1/category'; + +export async function handle(_req, res, { url }) { + const cursor = Number(url.searchParams.get('lastChangedCategory')) || 0; + const limit = Number(url.searchParams.get('limit')) || 20; + + const categories = await getCategoryList({ cursor, limit }); + const timestamp = serverTimestamp(); + + return sendJson( + res, + 200, + categories.map((category) => ({ + ...category, + updated_at: timestamp, + created_at: timestamp, + })) + ); +} diff --git a/src/endpoints/cimage.js b/src/endpoints/cimage.js new file mode 100644 index 0000000..b56287b --- /dev/null +++ b/src/endpoints/cimage.js @@ -0,0 +1,6 @@ +import { createImageHandler } from './image-handler.js'; + +export const method = 'GET'; +export const path = '/v1/cimage'; + +export const handle = createImageHandler(); diff --git a/src/endpoints/client.js b/src/endpoints/client.js new file mode 100644 index 0000000..0fc77f7 --- /dev/null +++ b/src/endpoints/client.js @@ -0,0 +1,71 @@ +import { sendJson, serverTimestamp } from '../http.js'; + +export const method = 'GET'; +export const path = '/v1/client'; + +function buildClientStep1(config) { + const { + authToken, + certificateFingerprint, + certificateSerialNumber, + mandantId, + serverFingerprint, + } = config; + + return { + authCode: null, + authToken, + certificateFingerprint, + certificateSerialNumber, + mandantId, + mandantName: null, + mandantDatabase: null, + serverFingerprint, + name: null, + serverTimestamp: serverTimestamp(), + }; +} + +function buildClientStep2(authCode, config) { + const { + authToken, + certificateFingerprint, + certificateSerialNumber, + mandantId, + mandantName, + mandantDatabase, + } = config; + + return { + authCode, + authToken, + certificateFingerprint, + certificateSerialNumber, + mandantId, + mandantName, + mandantDatabase, + serverFingerprint: null, + name: null, + serverTimestamp: serverTimestamp(), + }; +} + +export function handle(req, res, { url, pairingStore, config }) { + const authCode = url.searchParams.get('authCode') || ''; + const name = url.searchParams.get('name') || 'JTL-POS'; + + if (authCode.length <= 4 && authCode.length > 0) { + return sendJson(res, 200, buildClientStep1(config)); + } + + if (authCode.length === 6) { + if (pairingStore.hasPairingCode(authCode)) { + pairingStore.revokePairingCode(authCode); + pairingStore.registerDevice(config.authToken, name); + return sendJson(res, 200, buildClientStep2(authCode, config)); + } + return sendJson(res, 400, { Message: 'Der Authentifizierungscode ist falsch.' }); + } + + return sendJson(res, 400, { Message: 'Keinen passenden Authentifizierungscode gefunden.' }); +} diff --git a/src/endpoints/customergroup.js b/src/endpoints/customergroup.js new file mode 100644 index 0000000..0d91e1c --- /dev/null +++ b/src/endpoints/customergroup.js @@ -0,0 +1,13 @@ +import { sendJson } from '../http.js'; +import { getCustomerGroupList } from '../queries/customer-groups.js'; + +export const method = 'GET'; +export const path = '/v1/customergroup'; + +export async function handle(_req, res, { url }) { + const cursor = Number(url.searchParams.get('lastChangedCustomerGroup')) || 0; + + const customerGroups = await getCustomerGroupList({ cursor }); + + return sendJson(res, 200, customerGroups); +} diff --git a/src/endpoints/image-handler.js b/src/endpoints/image-handler.js new file mode 100644 index 0000000..5c92f60 --- /dev/null +++ b/src/endpoints/image-handler.js @@ -0,0 +1,21 @@ +import { sendBinary, sendJson } from '../http.js'; +import { resizeImage } from '../image-resize.js'; +import { getImageByHash } from '../queries/image.js'; + +export function createImageHandler() { + return async function handle(_req, res, { url }) { + const path = url.searchParams.get('path'); + + if (!path) { + return sendJson(res, 400, { Message: "Missing required query parameter 'path'." }); + } + + const image = await getImageByHash(path, '200'); + if (!image) { + return sendJson(res, 404, { Message: `No image was found for path '${path}'.` }); + } + + const buffer = await resizeImage(image.buffer); + return sendBinary(res, 200, buffer, image.contentType); + }; +} diff --git a/src/endpoints/index.js b/src/endpoints/index.js new file mode 100644 index 0000000..b3a39f3 --- /dev/null +++ b/src/endpoints/index.js @@ -0,0 +1,10 @@ +import * as category from './category.js'; +import * as cimage from './cimage.js'; +import * as client from './client.js'; +import * as customergroup from './customergroup.js'; +import * as init from './init.js'; +import * as order from './order.js'; +import * as pimage from './pimage.js'; +import * as product from './product.js'; + +export const endpoints = [client, init, category, product, pimage, cimage, customergroup, order]; diff --git a/src/endpoints/init.js b/src/endpoints/init.js new file mode 100644 index 0000000..8662d4c --- /dev/null +++ b/src/endpoints/init.js @@ -0,0 +1,33 @@ +import { sendJson } from '../http.js'; +import { getMaxExternalId } from '../order-log.js'; +import { getCategoryCount } from '../queries/category-count.js'; +import { getCustomerGroupCount } from '../queries/customer-groups.js'; +import { getProductCount } from '../queries/product-count.js'; + +export const method = 'GET'; +export const path = '/v1/init'; + +export async function handle(_req, res, { url }) { + const productCursor = Number(url.searchParams.get('lastChangedProduct')) || 0; + const categoryCursor = Number(url.searchParams.get('lastChangedCategory')) || 0; + const customerGroupCursor = Number(url.searchParams.get('lastChangedCustomerGroup')) || 0; + + const [productCount, categoryCount, customerGroupCount] = await Promise.all([ + getProductCount({ cursor: productCursor }), + getCategoryCount({ cursor: categoryCursor }), + getCustomerGroupCount({ cursor: customerGroupCursor }), + ]); + + return sendJson(res, 200, { + version: '1.10.12.0', + product_count: String(productCount), + category_count: String(categoryCount), + customer_count: '0', + customerGroup_count: String(customerGroupCount), + compositeProduct_count: '0', + configurationGroup_count: '0', + configurationItem_count: '0', + deletedEntity_count: '0', + max_orderId_count: getMaxExternalId(), + }); +} diff --git a/src/endpoints/order.js b/src/endpoints/order.js new file mode 100644 index 0000000..aa608c2 --- /dev/null +++ b/src/endpoints/order.js @@ -0,0 +1,48 @@ +import { sendJson } from '../http.js'; +import { logOrder } from '../order-log.js'; + +export const method = 'POST'; +export const path = '/v1/order'; + +function parseBody(buffer) { + if (!buffer || !buffer.length) { + return null; + } + try { + return JSON.parse(buffer.toString('utf8')); + } catch { + return undefined; + } +} + +function getOrders(body) { + if (!body || typeof body !== 'object') { + return []; + } + if (Array.isArray(body.orders)) { + return body.orders; + } + return []; +} + +export async function handle(req, res) { + const body = parseBody(req.rawBody); + + if (body === undefined) { + return sendJson(res, 500, []); + } + + const orders = getOrders(body); + const results = orders.map((order) => { + logOrder(order); + const externalOrderId = String(order?.externalId ?? ''); + + return { + status: 'OK', + externalOrderId, + message: '', + }; + }); + + return sendJson(res, 200, results); +} diff --git a/src/endpoints/pimage.js b/src/endpoints/pimage.js new file mode 100644 index 0000000..8242542 --- /dev/null +++ b/src/endpoints/pimage.js @@ -0,0 +1,6 @@ +import { createImageHandler } from './image-handler.js'; + +export const method = 'GET'; +export const path = '/v1/pimage'; + +export const handle = createImageHandler(); diff --git a/src/endpoints/product.js b/src/endpoints/product.js new file mode 100644 index 0000000..962db12 --- /dev/null +++ b/src/endpoints/product.js @@ -0,0 +1,67 @@ +import { sendJson } from '../http.js'; +import { getProductList } from '../queries/product-list.js'; + +export const method = 'GET'; +export const path = '/v1/product'; + +const STATIC_DEFAULTS = { + sort: '0', + p_price: '0.00', + discountable: '0', + deposit: '0', + discount: '', + d_price: '0.0', + tax_rate2: '', + use_in_out_tax: '0', + barcode: null, + use_stock: '0', + q_div: '0', + quantity: '0', + unit: null, + single_bookable: '0', + annotation: '', + status: '0', + tags: '', + is_parent: '0', + parent: '0', + variants: '', + print_kitchen_receipt: '0', + deposit_name: '', + updated_at: '0001-01-01 00:00:00', + isCompositeProduct: '0', + attributes: [], + configurationGroups: '', + options: null, + hasBestBeforeDate: '0', + hasLotNumber: '0', + hasSerialNumber: '0', + PLU: '', + short_description: '', + minStock: '0', + container: [], + reservedQuantity: '0.00', + deliveryDetails: [], + isbn: '', + manufacturerName: null, + han: null, + productType: '0', + voucherData: null, + inputPrice: '0', + inputQuantity: '0', +}; + +export async function handle(_req, res, { url }) { + const cursor = Number(url.searchParams.get('lastChangedProduct')) || 0; + const limit = Number(url.searchParams.get('limit')) || 20; + + const products = await getProductList({ cursor, limit }); + + return sendJson( + res, + 200, + products.map((product) => ({ + ...STATIC_DEFAULTS, + ...product, + })) + ); +} diff --git a/src/http.js b/src/http.js new file mode 100644 index 0000000..ee427cc --- /dev/null +++ b/src/http.js @@ -0,0 +1,35 @@ +export function serverTimestamp() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +export function readBody(req) { + return new Promise((resolve, reject) => { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); +} + +export function sendJson(res, statusCode, body) { + const responseBody = JSON.stringify(body); + res.writeHead(statusCode, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(responseBody), + }); + res.end(responseBody); +} + +export function sendBinary(res, statusCode, buffer, contentType) { + res.writeHead(statusCode, { + 'Content-Type': contentType, + 'Content-Length': buffer.length, + }); + res.end(buffer); +} + +export function normalizePath(pathname) { + return pathname.replace(/^\/api(?=\/v1\/)/, ''); +} diff --git a/src/image-resize.js b/src/image-resize.js new file mode 100644 index 0000000..3c7fdde --- /dev/null +++ b/src/image-resize.js @@ -0,0 +1,9 @@ +import sharp from 'sharp'; + +const MAX_DIMENSION = 200; + +export async function resizeImage(buffer) { + return sharp(buffer) + .resize(MAX_DIMENSION, MAX_DIMENSION, { fit: 'inside', withoutEnlargement: true }) + .toBuffer(); +} diff --git a/src/jtl-server.js b/src/jtl-server.js new file mode 100644 index 0000000..7f3e44b --- /dev/null +++ b/src/jtl-server.js @@ -0,0 +1,52 @@ +import { endpoints } from './endpoints/index.js'; +import { normalizePath, readBody, sendJson } from './http.js'; + +function buildConfig(config = {}) { + return { + authToken: config.authToken || process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241', + certificateFingerprint: + config.certificateFingerprint || + process.env.CERTIFICATE_FINGERPRINT || + 'BC2114CF407A42724BEEF417960F76DCBF9DE879', + certificateSerialNumber: + config.certificateSerialNumber || + process.env.CERTIFICATE_SERIAL_NUMBER || + '00BFC8BEACDB981B165210EF111CB9D3', + serverFingerprint: + config.serverFingerprint || + process.env.SERVER_FINGERPRINT || + '39-6D-BD-DE-F3-5C-5A-EA-C2-19-CF-EB-A7-A9-58-2F-20-3F-20-F7-3D-E6-CA-8E-AE-FD-28-30-37-A6-45-AE', + mandantId: config.mandantId || process.env.MANDANT_ID || '1', + mandantName: config.mandantName || process.env.MANDANT_NAME || 'eB-Standard', + mandantDatabase: config.mandantDatabase || process.env.MANDANT_DATABASE || 'eazybusiness', + }; +} + +export function createJtlPosServer(pairingStore, config = {}) { + const resolvedConfig = buildConfig(config); + const routes = new Map(endpoints.map((endpoint) => [`${endpoint.method} ${endpoint.path}`, endpoint])); + + async function handle(req, res) { + const url = new URL(req.url, 'https://localhost'); + const pathname = normalizePath(url.pathname); + const routeKey = `${req.method} ${pathname}`; + const endpoint = routes.get(routeKey); + + if (endpoint) { + return await endpoint.handle(req, res, { url, pairingStore, config: resolvedConfig }); + } + + return sendJson(res, 404, { + Message: `No HTTP resource was found that matches the request URI '${url}'.`, + }); + } + + return async function requestListener(req, res) { + req.rawBody = await readBody(req); + try { + await handle(req, res); + } catch (err) { + sendJson(res, 500, { Message: err.message }); + } + }; +} diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..f5abcb6 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,20 @@ +import chalk from 'chalk'; + +function timestamp() { + return chalk.gray(new Date().toISOString()); +} + +export const logger = { + info(...args) { + console.log(timestamp(), chalk.cyan('INFO'), ...args); + }, + success(...args) { + console.log(timestamp(), chalk.green('OK'), ...args); + }, + warn(...args) { + console.warn(timestamp(), chalk.yellow('WARN'), ...args); + }, + error(...args) { + console.error(timestamp(), chalk.red('ERROR'), ...args); + }, +}; diff --git a/src/order-log.js b/src/order-log.js new file mode 100644 index 0000000..aaafc12 --- /dev/null +++ b/src/order-log.js @@ -0,0 +1,28 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const ORDER_LOG_FILE = process.env.ORDER_LOG_FILE || path.join('logs', 'orders.log'); + +fs.mkdirSync(path.dirname(ORDER_LOG_FILE), { recursive: true }); +const stream = fs.createWriteStream(ORDER_LOG_FILE, { flags: 'a' }); + +let orderSequence = 0; +let maxExternalId = 0; + +export function logOrder(order) { + orderSequence += 1; + maxExternalId += 1; + + const line = `${new Date().toISOString()} #${orderSequence} externalId=${maxExternalId} ${JSON.stringify(order)}\n`; + stream.write(line); + + return maxExternalId; +} + +export function getMaxExternalId() { + return String(maxExternalId); +} + +export function closeOrderLog() { + stream.end(); +} diff --git a/src/pairing.js b/src/pairing.js new file mode 100644 index 0000000..55180e4 --- /dev/null +++ b/src/pairing.js @@ -0,0 +1,24 @@ +export function createPairingStore() { + /** @type {Record} */ + const authCodes = {}; + /** @type {Record} */ + const pairedDevices = {}; + + return { + setPairingCode(code, name = 'JTL-POS') { + authCodes[code] = { code, name, createdAt: Date.now() }; + }, + revokePairingCode(code) { + delete authCodes[code]; + }, + hasPairingCode(code) { + return Boolean(authCodes[code]); + }, + registerDevice(token, name = 'JTL-POS') { + pairedDevices[token] = { name, token, createdAt: Date.now() }; + }, + getPairedDevices() { + return { ...pairedDevices }; + }, + }; +} diff --git a/src/queries/category-count.js b/src/queries/category-count.js new file mode 100644 index 0000000..2feb5ac --- /dev/null +++ b/src/queries/category-count.js @@ -0,0 +1,18 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; +import { CATEGORY_TREE_CTE, categoryTreeRequest, getRootCategoryId } from './category-tree.js'; + +const CATEGORY_COUNT_SQL = ` +${CATEGORY_TREE_CTE} +SELECT COUNT(*) AS CategoryCount +FROM dbo.tKategorie k +WHERE k.kKategorie IN (SELECT kKategorie FROM CategoryTree) + AND CONVERT(BIGINT, k.bRowversion) > @cursor; +`; + +export async function getCategoryCount({ cursor = 0, rootCategoryId = getRootCategoryId() } = {}) { + const result = await categoryTreeRequest(getPool(), rootCategoryId) + .input('cursor', sql.BigInt, cursor) + .query(CATEGORY_COUNT_SQL); + return result.recordset[0]?.CategoryCount ?? 0; +} diff --git a/src/queries/category-list.js b/src/queries/category-list.js new file mode 100644 index 0000000..81cb881 --- /dev/null +++ b/src/queries/category-list.js @@ -0,0 +1,48 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; +import { CATEGORY_TREE_CTE, categoryTreeRequest, getRootCategoryId } from './category-tree.js'; + +const LANGUAGE_ID = Number(process.env.LANGUAGE_ID) || 1; +const IMAGE_PLATFORM_ID = Number(process.env.IMAGE_PLATFORM_ID) || 1; +const IMAGE_SHOP_ID = Number(process.env.IMAGE_SHOP_ID) || 0; + +const CATEGORY_LIST_SQL = ` +${CATEGORY_TREE_CTE} +SELECT TOP (@limit) + k.kKategorie AS id, + k.kOberKategorie AS pid, + k.nSort AS sort, + ks.cName AS name, + b.cHash AS imgHash, + CONVERT(BIGINT, k.bRowversion) AS lastChanged +FROM dbo.tKategorie k +INNER JOIN dbo.tKategorieSprache ks ON ks.kKategorie = k.kKategorie AND ks.kSprache = @languageId +LEFT JOIN dbo.tKategoriebildPlattform kbp + ON kbp.kKategorie = k.kKategorie AND kbp.kPlattform = @imagePlatformId AND kbp.kShop = @imageShopId +LEFT JOIN dbo.tBild b ON b.kBild = kbp.kBild +WHERE k.kKategorie IN (SELECT kKategorie FROM CategoryTree WHERE kKategorie <> @rootCategoryId) + AND k.cAktiv = 'Y' + AND CONVERT(BIGINT, k.bRowversion) > @cursor +ORDER BY lastChanged ASC; +`; + +export async function getCategoryList({ cursor = 0, limit = 20, rootCategoryId = getRootCategoryId() } = {}) { + const result = await categoryTreeRequest(getPool(), rootCategoryId) + .input('cursor', sql.BigInt, cursor) + .input('limit', sql.Int, limit) + .input('languageId', sql.Int, LANGUAGE_ID) + .input('imagePlatformId', sql.Int, IMAGE_PLATFORM_ID) + .input('imageShopId', sql.Int, IMAGE_SHOP_ID) + .query(CATEGORY_LIST_SQL); + + return result.recordset.map((row) => ({ + _id: String(row.id), + imghash: row.imgHash ?? null, + imgsrc: row.imgHash ?? null, + name: row.name, + pid: String(row.pid), + discounts: [], + sort: String(row.sort), + lastChanged: String(row.lastChanged), + })); +} diff --git a/src/queries/category-tree.js b/src/queries/category-tree.js new file mode 100644 index 0000000..7a52409 --- /dev/null +++ b/src/queries/category-tree.js @@ -0,0 +1,18 @@ +import sql from 'mssql'; + +export const CATEGORY_TREE_CTE = ` +WITH CategoryTree AS ( + SELECT kKategorie FROM dbo.tKategorie WHERE kKategorie = @rootCategoryId + UNION ALL + SELECT t.kKategorie + FROM dbo.tKategorie t + INNER JOIN CategoryTree ct ON t.kOberKategorie = ct.kKategorie +)`; + +export function getRootCategoryId() { + return Number(process.env.ROOT_CATEGORY_ID) || 1; +} + +export function categoryTreeRequest(pool, rootCategoryId = getRootCategoryId()) { + return pool.request().input('rootCategoryId', sql.Int, rootCategoryId); +} diff --git a/src/queries/customer-groups.js b/src/queries/customer-groups.js new file mode 100644 index 0000000..972cf6c --- /dev/null +++ b/src/queries/customer-groups.js @@ -0,0 +1,48 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; + +const CUSTOMER_GROUP_IDS_SQL = ` +SELECT kKundenGruppe +FROM dbo.tKundenGruppe +ORDER BY kKundenGruppe; +`; + +const CUSTOMER_GROUP_LIST_SQL = ` +SELECT + kKundenGruppe AS id, + cName AS name, + nStandard AS standard, + fRabatt AS discountPercent, + CONVERT(BIGINT, bRowversion) AS lastChanged +FROM dbo.tKundenGruppe +WHERE CONVERT(BIGINT, bRowversion) > @cursor +ORDER BY lastChanged ASC; +`; + +const CUSTOMER_GROUP_COUNT_SQL = ` +SELECT COUNT(*) AS CustomerGroupCount +FROM dbo.tKundenGruppe +WHERE CONVERT(BIGINT, bRowversion) > @cursor; +`; + +export async function getCustomerGroupIds() { + const result = await getPool().request().query(CUSTOMER_GROUP_IDS_SQL); + return result.recordset.map((row) => row.kKundenGruppe); +} + +export async function getCustomerGroupList({ cursor = 0 } = {}) { + const result = await getPool().request().input('cursor', sql.BigInt, cursor).query(CUSTOMER_GROUP_LIST_SQL); + + return result.recordset.map((row) => ({ + customerGroupId: String(row.id), + name: row.name, + standard: String(row.standard), + discountPercent: Number(row.discountPercent).toFixed(2), + lastChanged: String(row.lastChanged), + })); +} + +export async function getCustomerGroupCount({ cursor = 0 } = {}) { + const result = await getPool().request().input('cursor', sql.BigInt, cursor).query(CUSTOMER_GROUP_COUNT_SQL); + return result.recordset[0]?.CustomerGroupCount ?? 0; +} diff --git a/src/queries/image.js b/src/queries/image.js new file mode 100644 index 0000000..c6fd14c --- /dev/null +++ b/src/queries/image.js @@ -0,0 +1,43 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; + +const IMAGE_BY_HASH_SQL = ` +SELECT bBild, bVorschauBild, nBreite, nHoehe, nVorschauBreite, nVorschauHoehe, cQuelle +FROM dbo.tBild +WHERE cHash = @hash; +`; + +function contentTypeFor(cQuelle) { + const extension = (cQuelle || '').split('.').pop()?.toLowerCase(); + switch (extension) { + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'webp': + return 'image/webp'; + case 'jpg': + case 'jpeg': + default: + return 'image/jpeg'; + } +} + +export async function getImageByHash(hash, size) { + const result = await getPool().request().input('hash', sql.NVarChar, hash).query(IMAGE_BY_HASH_SQL); + + const row = result.recordset[0]; + if (!row) { + return null; + } + + const previewMaxDimension = Math.max(row.nVorschauBreite || 0, row.nVorschauHoehe || 0); + const useFull = !previewMaxDimension || Number(size) > previewMaxDimension; + const buffer = useFull ? row.bBild : row.bVorschauBild; + + if (!buffer) { + return null; + } + + return { buffer, contentType: contentTypeFor(row.cQuelle) }; +} diff --git a/src/queries/product-attributes.js b/src/queries/product-attributes.js new file mode 100644 index 0000000..4611e4e --- /dev/null +++ b/src/queries/product-attributes.js @@ -0,0 +1,149 @@ +const PFAND_ATTR_IDS = [ + 'Pfandartikel', + 'Pfandart (Bezeichnung auf Ausdruck)', + 'Pfandbetrag', +]; + +let pfandMetaCache = null; +let activeShopsCache = null; + +async function getPfandMetadata(pool) { + if (!pfandMetaCache) { + const result = await pool.request().query(` + SELECT + a.kAttribut, + a.cAttributId, + a.nSortierung, + a.kFeldTyp, + a.cGruppeName, + s.cName + FROM dbo.tAttribut a + INNER JOIN dbo.tAttributSprache s ON s.kAttribut = a.kAttribut AND s.kSprache = 0 + WHERE a.cGruppeName = 'JTL-POS' + AND a.cAttributId IN ('Pfandartikel', 'Pfandart (Bezeichnung auf Ausdruck)', 'Pfandbetrag') + `); + pfandMetaCache = result.recordset; + } + return pfandMetaCache; +} + +async function getActiveShops(pool) { + if (!activeShopsCache) { + const result = await pool.request().query(` + WITH ActiveShops AS ( + SELECT ss.kShop, s.kKategorie AS rootKategorie + FROM dbo.tShopSubshop ss + INNER JOIN dbo.tShop s ON s.kShop = ss.kShop + WHERE ss.nGesperrt = 0 + ) + SELECT DISTINCT ash.kShop FROM ActiveShops ash + `); + activeShopsCache = result.recordset.map((row) => row.kShop); + } + return activeShopsCache; +} + +function attributesSql(articleIds, activeShops, pfandKAttribute) { + const shopList = activeShops.length ? activeShops.join(',') : 'NULL'; + const pfandList = pfandKAttribute.length ? pfandKAttribute.join(',') : 'NULL'; + + return ` + SELECT + aa.kArtikel AS articleId, + aa.kAttribut, + aa.kShop, + at.nSortierung, + at.kFeldTyp, + at.cGruppeName, + at.cAttributId, + ats.cName, + aas.cWertVarchar, + aas.nWertInt, + aas.fWertDecimal + FROM dbo.tArtikelAttribut aa + INNER JOIN dbo.tArtikelAttributSprache aas + ON aas.kArtikelAttribut = aa.kArtikelAttribut AND aas.kSprache = 0 + INNER JOIN dbo.tAttribut at ON at.kAttribut = aa.kAttribut + INNER JOIN dbo.tAttributSprache ats ON ats.kAttribut = at.kAttribut AND ats.kSprache = 0 + WHERE aa.kArtikel IN (${articleIds.join(',')}) + AND ( + aa.kShop = 0 + OR ( + aa.kShop IN (${shopList}) + AND aa.kAttribut IN (${pfandList}) + ) + ) + ORDER BY aa.kArtikel, at.nSortierung, aa.kShop; + `; +} + +function mergeArticleAttributes(rows) { + const byAttribute = new Map(); + for (const row of rows) { + byAttribute.set(row.kAttribut, row); + } + return [...byAttribute.values()].sort((a, b) => a.nSortierung - b.nSortierung); +} + +function toPosAttribute(row) { + return { + aname: row.cName, + aprice: '0.0', + asort: String(row.nSortierung), + atype: String(row.kFeldTyp), + agroup: row.cGruppeName ?? '', + }; +} + +function deriveDepositFields(rows) { + const byId = new Map(rows.map((row) => [row.cAttributId, row])); + const pfandArtikel = byId.get('Pfandartikel'); + const pfandArt = byId.get('Pfandart (Bezeichnung auf Ausdruck)'); + const pfandBetrag = byId.get('Pfandbetrag'); + + if (!pfandArtikel && !pfandArt && !pfandBetrag) { + return null; + } + + return { + deposit: pfandArtikel?.nWertInt === 1 ? '1' : '0', + deposit_name: pfandArt?.cWertVarchar ?? '', + d_price: + pfandBetrag?.fWertDecimal != null + ? Number(pfandBetrag.fWertDecimal).toFixed(2) + : '0.0', + }; +} + +export async function getProductAttributes(pool, articleIds) { + if (!articleIds.length) { + return new Map(); + } + + const [pfandMeta, activeShops] = await Promise.all([ + getPfandMetadata(pool), + getActiveShops(pool), + ]); + const pfandKAttribute = pfandMeta.map((row) => row.kAttribut); + + const result = await pool.request().query(attributesSql(articleIds, activeShops, pfandKAttribute)); + + const rowsByArticle = new Map(); + for (const row of result.recordset) { + if (!rowsByArticle.has(row.articleId)) { + rowsByArticle.set(row.articleId, []); + } + rowsByArticle.get(row.articleId).push(row); + } + + const attributesByArticle = new Map(); + for (const [articleId, rows] of rowsByArticle) { + const merged = mergeArticleAttributes(rows); + attributesByArticle.set(articleId, { + attributes: merged.map(toPosAttribute), + deposit: deriveDepositFields(merged), + }); + } + + return attributesByArticle; +} diff --git a/src/queries/product-count.js b/src/queries/product-count.js new file mode 100644 index 0000000..a482736 --- /dev/null +++ b/src/queries/product-count.js @@ -0,0 +1,20 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; +import { CATEGORY_TREE_CTE, categoryTreeRequest, getRootCategoryId } from './category-tree.js'; + +const PRODUCT_COUNT_SQL = ` +${CATEGORY_TREE_CTE} +SELECT COUNT(DISTINCT a.kArtikel) AS ProductCount +FROM dbo.tArtikel a +INNER JOIN dbo.tKategorieArtikel ka ON ka.kArtikel = a.kArtikel +WHERE a.cAktiv = 'Y' + AND ka.kKategorie IN (SELECT kKategorie FROM CategoryTree) + AND CONVERT(BIGINT, a.bRowversion) > @cursor; +`; + +export async function getProductCount({ cursor = 0, rootCategoryId = getRootCategoryId() } = {}) { + const result = await categoryTreeRequest(getPool(), rootCategoryId) + .input('cursor', sql.BigInt, cursor) + .query(PRODUCT_COUNT_SQL); + return result.recordset[0]?.ProductCount ?? 0; +} diff --git a/src/queries/product-list.js b/src/queries/product-list.js new file mode 100644 index 0000000..3adf775 --- /dev/null +++ b/src/queries/product-list.js @@ -0,0 +1,151 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; +import { getCustomerGroupIds } from './customer-groups.js'; +import { getProductAttributes } from './product-attributes.js'; + +const LANGUAGE_ID = Number(process.env.LANGUAGE_ID) || 1; +const TAX_ZONE_NAME = process.env.TAX_ZONE_NAME || 'Zone-EU'; +const IMAGE_PLATFORM_ID = Number(process.env.IMAGE_PLATFORM_ID) || 1; +const IMAGE_SHOP_ID = Number(process.env.IMAGE_SHOP_ID) || 0; + +const PRODUCT_LIST_SQL = ` +WITH TaxRates AS ( + SELECT kSteuerklasse, fSteuersatz + FROM dbo.tSteuersatz + WHERE kSteuerzone IN (SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = @taxZoneName) +) +SELECT TOP (@limit) + a.kArtikel AS id, + a.cArtNr AS sku, + ab.cName AS name, + a.fVKNetto AS netPrice, + tr.fSteuersatz AS taxRate, + a.dErstelldatum AS createdAt, + CONVERT(BIGINT, a.bRowversion) AS lastChanged, + ( + SELECT TOP 1 img.cHash + FROM dbo.tArtikelbildPlattform abp + INNER JOIN dbo.tBild img ON img.kBild = abp.kBild + WHERE abp.kArtikel = a.kArtikel + AND abp.kPlattform = @imagePlatformId + AND abp.kShop = @imageShopId + ORDER BY abp.nNr + ) AS imgHash, + ( + SELECT STRING_AGG(CAST(ka.kKategorie AS varchar(20)), ',') + FROM dbo.tkategorieartikel ka + WHERE ka.kArtikel = a.kArtikel + ) AS categoryIds, + a.nIstVater AS isParent, + a.kVaterArtikel AS parentArticleId, + ( + SELECT TOP 1 pv.cVariantName + FROM Pos.vProductVariant pv + WHERE pv.kProduct = a.kArtikel + ) AS variantName +FROM dbo.tArtikel a +INNER JOIN dbo.tArtikelBeschreibung ab ON ab.kArtikel = a.kArtikel AND ab.kSprache = @languageId +LEFT JOIN TaxRates tr ON tr.kSteuerklasse = a.kSteuerklasse +WHERE a.cAktiv = 'Y' + AND CONVERT(BIGINT, a.bRowversion) > @cursor +ORDER BY lastChanged ASC; +`; + +function priceOverridesSql(articleIds) { + const idList = articleIds.join(','); + return ` + SELECT p.kArtikel AS articleId, p.kKundenGruppe AS customerGroupId, MIN(pd.fNettoPreis) AS netPrice + FROM dbo.tPreis p + INNER JOIN dbo.tPreisDetail pd ON pd.kPreis = p.kPreis + WHERE p.kArtikel IN (${idList}) AND p.kShop = 0 AND pd.nAnzahlAb = 0 + GROUP BY p.kArtikel, p.kKundenGruppe; + `; +} + +function formatDateTime(date) { + if (!date) { + return '0001-01-01 00:00:00'; + } + const pad = (n) => String(n).padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + +function grossPrice(netPrice, taxRate) { + return (Number(netPrice) * (1 + Number(taxRate || 0) / 100)).toFixed(2); +} + +export async function getProductList({ cursor = 0, limit = 20 } = {}) { + const pool = getPool(); + + const [productResult, customerGroupIds] = await Promise.all([ + pool + .request() + .input('cursor', sql.BigInt, cursor) + .input('limit', sql.Int, limit) + .input('languageId', sql.Int, LANGUAGE_ID) + .input('taxZoneName', sql.NVarChar, TAX_ZONE_NAME) + .input('imagePlatformId', sql.Int, IMAGE_PLATFORM_ID) + .input('imageShopId', sql.Int, IMAGE_SHOP_ID) + .query(PRODUCT_LIST_SQL), + getCustomerGroupIds(), + ]); + + const products = productResult.recordset; + const articleIds = products.map((p) => p.id); + + const overridesByArticle = new Map(); + let attributesByArticle = new Map(); + if (articleIds.length > 0) { + const [overrideResult, attributeMap] = await Promise.all([ + pool.request().query(priceOverridesSql(articleIds)), + getProductAttributes(pool, articleIds), + ]); + attributesByArticle = attributeMap; + for (const row of overrideResult.recordset) { + if (!overridesByArticle.has(row.articleId)) { + overridesByArticle.set(row.articleId, new Map()); + } + overridesByArticle.get(row.articleId).set(row.customerGroupId, row.netPrice); + } + } + + return products.map((product) => { + const overrides = overridesByArticle.get(product.id); + const basePrice = grossPrice(product.netPrice, product.taxRate); + + const prices = customerGroupIds.map((customerGroupId) => { + const overrideNetPrice = overrides?.get(customerGroupId); + const price = + overrideNetPrice !== undefined ? grossPrice(overrideNetPrice, product.taxRate) : basePrice; + return { + customerGroupId: String(customerGroupId), + customerId: '0', + price, + quantity: '0', + }; + }); + + const categoryIds = product.categoryIds ? product.categoryIds.split(',') : []; + const articleAttributes = attributesByArticle.get(product.id); + + return { + _id: String(product.id), + imghash: product.imgHash ?? null, + imgsrc: product.imgHash ?? null, + sku: product.sku, + name: product.name, + tax_rate: String(Math.round(Number(product.taxRate || 0))), + price: basePrice, + created_at: formatDateTime(product.createdAt), + lastChanged: String(product.lastChanged), + categories_id: categoryIds[0] ?? '0', + categories: categoryIds.map((categoryId) => ({ categoryId })), + prices, + is_parent: product.isParent ? '1' : '0', + parent: product.parentArticleId > 0 ? String(product.parentArticleId) : '0', + variants: product.variantName ?? '', + attributes: articleAttributes?.attributes ?? [], + ...(articleAttributes?.deposit ?? {}), + }; + }); +} diff --git a/src/request-log.js b/src/request-log.js new file mode 100644 index 0000000..545756b --- /dev/null +++ b/src/request-log.js @@ -0,0 +1,20 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const LOG_FILE = process.env.LOG_FILE || path.join('logs', 'requests.log'); + +fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true }); +const stream = fs.createWriteStream(LOG_FILE, { flags: 'a' }); + +function isoTimestamp() { + return new Date().toISOString(); +} + +export function logRequest({ remoteAddress, method, url, statusCode, durationMs, response }) { + const line = `${isoTimestamp()} ${remoteAddress} ${method} ${url} ${statusCode} ${durationMs}ms ${response}\n`; + stream.write(line); +} + +export function closeRequestLog() { + stream.end(); +} diff --git a/test-client.js b/test-client.js new file mode 100644 index 0000000..b4954aa --- /dev/null +++ b/test-client.js @@ -0,0 +1,130 @@ +import https from 'node:https'; +import { gunzipSync } from 'node:zlib'; + +const HOST = process.env.JTL_HOST || '127.0.0.1'; +const PORT = Number(process.env.JTL_PORT) || Number(process.env.PORT) || 4443; + +const INIT_QUERY = + 'mandantId=1&lastChangedCategory=0&lastChangedCustomer=0&lastChangedCustomerGroup=0&lastChangedProduct=0&lastChangedConfigurationGroup=0&lastChangedConfigurationItem=0&lastChangedCompositeProduct=0&lastChangedDeletedEntity=0'; + +function usage() { + console.error('Usage:'); + console.error(' node test-client.js [name] # 6-digit: step1+2, 4-digit: step1 only'); + console.error(' node test-client.js init '); + process.exit(1); +} + +function posHeaders(authToken) { + const headers = { + accept: 'application/json', + 'content-type': 'application/json', + 'cache-control': 'no-cache', + version: '1.0.11.14', + system: 'JTL-POS', + charset: 'utf-8', + connection: 'Keep-Alive', + 'user-agent': 'Dalvik/2.1.0 (Linux; U; Android 13; SM-T970 Build/TP1A.220624.014)', + 'accept-encoding': 'gzip', + }; + + if (authToken) { + headers.authorization = `Bearer ${authToken}`; + } + + return headers; +} + +function decodeBody(buffer, headers) { + if (headers['content-encoding'] === 'gzip') { + return gunzipSync(buffer); + } + return buffer; +} + +function request(path, authToken) { + return new Promise((resolve, reject) => { + const req = https.request( + { + hostname: HOST, + port: PORT, + path, + method: 'GET', + headers: posHeaders(authToken), + rejectUnauthorized: false, + }, + (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks); + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: decodeBody(raw, res.headers), + }); + }); + } + ); + + req.on('error', reject); + req.end(); + }); +} + +function printResponse(res) { + console.log(`status: ${res.statusCode}`); + console.log('headers:', JSON.stringify(res.headers, null, 2)); + console.log('body:'); + try { + console.log(JSON.stringify(JSON.parse(res.body.toString('utf8')), null, 2)); + } catch { + console.log(res.body.toString('utf8')); + } +} + +async function clientRequest(authCode, name, label) { + const path = `/api/v1/client?authCode=${encodeURIComponent(authCode)}&name=${encodeURIComponent(name)}`; + console.log(`${label} GET https://${HOST}:${PORT}${path}\n`); + const res = await request(path); + printResponse(res); + console.log(); + return res; +} + +async function main() { + const mode = process.argv[2]; + + if (!mode) { + usage(); + } + + if (mode === 'init') { + const authToken = process.argv[3] || process.env.AUTH_TOKEN; + if (!authToken) { + usage(); + } + + const path = `/v1/init?${INIT_QUERY}`; + console.log(`GET https://${HOST}:${PORT}${path}\n`); + + const res = await request(path, authToken); + printResponse(res); + return; + } + + const name = process.argv[3] || process.env.CLIENT_NAME || '001'; + const fullCode = mode; + + if (fullCode.length === 6) { + await clientRequest(fullCode.slice(0, 4), name, '=== step 1 (4 digits) ==='); + await clientRequest(fullCode, name, '=== step 2 (6 digits) ==='); + return; + } + + await clientRequest(fullCode, name, '=== step 1 (4 digits) ==='); +} + +main().catch((err) => { + console.error('request failed:', err.message); + process.exit(1); +});