diff --git a/changelog.md b/changelog.md index ef3391b..284231e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,21 @@ # Changelog +## 2025-11-21 - 2.3.0 - feat(smarts3-server) +Introduce native custom S3 server implementation (Smarts3Server) with routing, middleware, context, filesystem store, controllers and XML utilities; add SmartXml and AWS SDK test; keep optional legacy s3rver backend. + +- Add Smarts3Server: native, Node.js http-based S3-compatible server (ts/classes/smarts3-server.ts) +- New routing and middleware system: S3Router and MiddlewareStack for pattern matching and middleware composition (ts/classes/router.ts, ts/classes/middleware-stack.ts) +- Introduce request context and helpers: S3Context for parsing requests, sending responses and XML (ts/classes/context.ts) +- Filesystem-backed storage: FilesystemStore with bucket/object operations, streaming uploads, MD5 handling and Windows-safe key encoding (ts/classes/filesystem-store.ts) +- S3 error handling: S3Error class that maps S3 error codes and produces XML error responses (ts/classes/s3-error.ts) +- Controllers for service, bucket and object operations with S3-compatible XML responses and copy/range support (ts/controllers/*.ts) +- XML utilities and SmartXml integration for consistent XML generation/parsing (ts/utils/xml.utils.ts, ts/plugins.ts) +- Expose native plugins (http, crypto, url, fs) and SmartXml via plugins.ts +- ts/index.ts: add useCustomServer option, default to custom server, export Smarts3Server and handle start/stop for both custom and legacy backends +- Add AWS SDK v3 integration test (test/test.aws-sdk.node.ts) to validate compatibility +- package.json: add @aws-sdk/client-s3 devDependency and @push.rocks/smartxml dependency +- Documentation: readme.md updated to describe native custom server and legacy s3rver compatibility + ## 2025-11-20 - 2.2.7 - fix(core) Update dependencies, code style and project config; add pnpm overrides and ignore AI folders diff --git a/package.json b/package.json index 3064c17..a2d4e90 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "buildDocs": "tsdoc" }, "devDependencies": { + "@aws-sdk/client-s3": "^3.936.0", "@git.zone/tsbuild": "^3.1.0", "@git.zone/tsbundle": "^2.5.2", "@git.zone/tsrun": "^2.0.0", @@ -39,6 +40,7 @@ "@push.rocks/smartbucket": "^3.3.10", "@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartpath": "^6.0.0", + "@push.rocks/smartxml": "^1.0.6", "@tsclass/tsclass": "^9.3.0", "@types/s3rver": "^3.7.0", "s3rver": "^3.7.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca171e2..cf61cc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@push.rocks/smartpath': specifier: ^6.0.0 version: 6.0.0 + '@push.rocks/smartxml': + specifier: ^1.0.6 + version: 1.1.1 '@tsclass/tsclass': specifier: ^9.3.0 version: 9.3.0 @@ -27,6 +30,9 @@ importers: specifier: ^3.7.1 version: 3.7.1 devDependencies: + '@aws-sdk/client-s3': + specifier: ^3.936.0 + version: 3.936.0 '@git.zone/tsbuild': specifier: ^3.1.0 version: 3.1.0 @@ -38,10 +44,10 @@ importers: version: 2.0.0 '@git.zone/tstest': specifier: ^3.0.0 - version: 3.0.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.9.3) + version: 3.0.1(socks@2.8.7)(typescript@5.9.3) '@types/node': specifier: ^22.9.0 - version: 22.17.2 + version: 22.19.1 packages: @@ -83,54 +89,26 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-cognito-identity@3.864.0': - resolution: {integrity: sha512-IH3RSg/Zy2+yXQ2d4jmMk2U8A+BuJ9uNUYPWAg144yUUxanN1Czb+GyFKeJO4NGhVnn5D+j1YoRLpJN8PW2B0g==} - engines: {node: '>=18.0.0'} - '@aws-sdk/client-s3@3.936.0': resolution: {integrity: sha512-dnzZAkJDa9tdCxhqdnh37hdizJkernoFn0rufWahziOEmf0Yv9+mLeqR4qDmsAGUMuD1jFCmPR97FaCoh10mZg==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.864.0': - resolution: {integrity: sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==} - engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.936.0': resolution: {integrity: sha512-0G73S2cDqYwJVvqL08eakj79MZG2QRaB56Ul8/Ps9oQxllr7DMI1IQ/N3j3xjxgpq/U36pkoFZ8aK1n7Sbr3IQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.864.0': - resolution: {integrity: sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==} - engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.936.0': resolution: {integrity: sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-cognito-identity@3.864.0': - resolution: {integrity: sha512-jF6xJS67nPvJ/ElvdA2Q/EDArTcd0fKS3R6zImupOkTMm9PwmEM/BM7hpQCUFkVcaUhtvPpYCtuolGq9ezuKng==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-env@3.864.0': - resolution: {integrity: sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-env@3.936.0': resolution: {integrity: sha512-dKajFuaugEA5i9gCKzOaVy9uTeZcApE+7Z5wdcZ6j40523fY1a56khDAUYkCfwqa7sHci4ccmxBkAo+fW1RChA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.864.0': - resolution: {integrity: sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.936.0': resolution: {integrity: sha512-5FguODLXG1tWx/x8fBxH+GVrk7Hey2LbXV5h9SFzYCx/2h50URBm0+9hndg0Rd23+xzYe14F6SI9HA9c1sPnjg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.864.0': - resolution: {integrity: sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.936.0': resolution: {integrity: sha512-TbUv56ERQQujoHcLMcfL0Q6bVZfYF83gu/TjHkVkdSlHPOIKaG/mhE2XZSQzXv1cud6LlgeBbfzVAxJ+HPpffg==} engines: {node: '>=18.0.0'} @@ -139,42 +117,22 @@ packages: resolution: {integrity: sha512-8DVrdRqPyUU66gfV7VZNToh56ZuO5D6agWrkLQE/xbLJOm2RbeRgh6buz7CqV8ipRd6m+zCl9mM4F3osQLZn8Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.864.0': - resolution: {integrity: sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.936.0': resolution: {integrity: sha512-rk/2PCtxX9xDsQW8p5Yjoca3StqmQcSfkmD7nQ61AqAHL1YgpSQWqHE+HjfGGiHDYKG7PvE33Ku2GyA7lEIJAw==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.864.0': - resolution: {integrity: sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.936.0': resolution: {integrity: sha512-GpA4AcHb96KQK2PSPUyvChvrsEKiLhQ5NWjeef2IZ3Jc8JoosiedYqp6yhZR+S8cTysuvx56WyJIJc8y8OTrLA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.864.0': - resolution: {integrity: sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.936.0': resolution: {integrity: sha512-wHlEAJJvtnSyxTfNhN98JcU4taA1ED2JvuI2eePgawqBwS/Tzi0mhED1lvNIaWOkjfLd+nHALwszGrtJwEq4yQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.864.0': - resolution: {integrity: sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.936.0': resolution: {integrity: sha512-v3qHAuoODkoRXsAF4RG+ZVO6q2P9yYBT4GMpMEfU9wXVNn7AIfwZgTwzSUfnjNiGva5BKleWVpRpJ9DeuLFbUg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-providers@3.864.0': - resolution: {integrity: sha512-k4K7PzvHpdHQLczgWT26Yk6t+VBwZ35jkIQ3dKODvBjfzlYHTX0y+VgemmDWrat1ahKfYb/OAw/gdwmnyxsAsw==} - engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-bucket-endpoint@3.936.0': resolution: {integrity: sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==} engines: {node: '>=18.0.0'} @@ -187,10 +145,6 @@ packages: resolution: {integrity: sha512-l3GG6CrSQtMCM6fWY7foV3JQv0WJWT+3G6PSP3Ceb/KEE/5Lz5PrYFXTBf+bVoYL1b0bGjGajcgAXpstBmtHtQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-host-header@3.862.0': - resolution: {integrity: sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==} - engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-host-header@3.936.0': resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} engines: {node: '>=18.0.0'} @@ -199,18 +153,10 @@ packages: resolution: {integrity: sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-logger@3.862.0': - resolution: {integrity: sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==} - engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-logger@3.936.0': resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.862.0': - resolution: {integrity: sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==} - engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.936.0': resolution: {integrity: sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==} engines: {node: '>=18.0.0'} @@ -223,26 +169,14 @@ packages: resolution: {integrity: sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.864.0': - resolution: {integrity: sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==} - engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.936.0': resolution: {integrity: sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.864.0': - resolution: {integrity: sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.936.0': resolution: {integrity: sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A==} engines: {node: '>=18.0.0'} - '@aws-sdk/region-config-resolver@3.862.0': - resolution: {integrity: sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==} - engines: {node: '>=18.0.0'} - '@aws-sdk/region-config-resolver@3.936.0': resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} engines: {node: '>=18.0.0'} @@ -251,18 +185,10 @@ packages: resolution: {integrity: sha512-8qS0GFUqkmwO7JZ0P8tdluBmt1UTfYUah8qJXGzNh9n1Pcb0AIeT117cCSiCUtwk+gDbJvd4hhRIhJCNr5wgjg==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.864.0': - resolution: {integrity: sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==} - engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.936.0': resolution: {integrity: sha512-vvw8+VXk0I+IsoxZw0mX9TMJawUJvEsg3EF7zcCSetwhNPAU8Xmlhv7E/sN/FgSmm7b7DsqKoW6rVtQiCs1PWQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.862.0': - resolution: {integrity: sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==} - engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.936.0': resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} engines: {node: '>=18.0.0'} @@ -271,10 +197,6 @@ packages: resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-endpoints@3.862.0': - resolution: {integrity: sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/util-endpoints@3.936.0': resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} engines: {node: '>=18.0.0'} @@ -283,21 +205,9 @@ packages: resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-user-agent-browser@3.862.0': - resolution: {integrity: sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==} - '@aws-sdk/util-user-agent-browser@3.936.0': resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} - '@aws-sdk/util-user-agent-node@3.864.0': - resolution: {integrity: sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - '@aws-sdk/util-user-agent-node@3.936.0': resolution: {integrity: sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw==} engines: {node: '>=18.0.0'} @@ -307,16 +217,12 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.862.0': - resolution: {integrity: sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/xml-builder@3.930.0': resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} engines: {node: '>=18.0.0'} - '@aws/lambda-invoke-store@0.2.0': - resolution: {integrity: sha512-D1jAmAZQYMoPiacfgNf7AWhg3DFN3Wq/vQv3WINt9znwjzHp2x+WzdJFxxj7xZL7V1U79As6G8f7PorMYWBKsQ==} + '@aws/lambda-invoke-store@0.2.1': + resolution: {integrity: sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==} engines: {node: '>=18.0.0'} '@babel/code-frame@7.27.1': @@ -344,8 +250,8 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@dabh/diagnostics@2.0.3': - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} '@design.estate/dees-comms@1.0.27': resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} @@ -537,8 +443,8 @@ packages: resolution: {integrity: sha512-yA6zCjL+kn7xfZe6sL/m4K+zYqgkznG/pF6++i/E17iwzpG6dHmW+VZmYldHe86sW4DcLMvqM6CxM+KlgaEpKw==} hasBin: true - '@git.zone/tstest@3.0.0': - resolution: {integrity: sha512-r82PfKaRj8J37I+rJbKP376k8PEiyYStbXC4q+deqVVZqfESDhghu4prsUVakqlHGCBxnjk+zrWOD4G9o5fveQ==} + '@git.zone/tstest@3.0.1': + resolution: {integrity: sha512-YjjLLWGj8fE8yYAfMrLSDgdZ+JJOS7I6iRshIyr6THH5dnTONOA3R076zBaryRw58qgPn+s/0jno7wlhYhv0iw==} hasBin: true '@happy-dom/global-registrator@15.11.7': @@ -597,6 +503,22 @@ packages: '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@oozcitak/dom@1.15.10': + resolution: {integrity: sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==} + engines: {node: '>=8.0'} + + '@oozcitak/infra@1.0.8': + resolution: {integrity: sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==} + engines: {node: '>=6.0'} + + '@oozcitak/url@1.0.4': + resolution: {integrity: sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==} + engines: {node: '>=8.0'} + + '@oozcitak/util@8.3.8': + resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} + engines: {node: '>=8.0'} + '@oxc-project/types@0.98.0': resolution: {integrity: sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw==} @@ -825,8 +747,8 @@ packages: '@push.rocks/smartrx@3.0.10': resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==} - '@push.rocks/smarts3@2.2.6': - resolution: {integrity: sha512-f2i2keHs+KZr5cyB8nBOnmRGiE2YG42W4pSx+8gmZEsf8yZUT1iUnuD/YZVTKosH2v5dPCKdmtSpMSux8Q/tCw==} + '@push.rocks/smarts3@2.2.7': + resolution: {integrity: sha512-9ZXGMlmUL2Wd+YJO0xOB8KyqPf4V++fWJvTq4s76bnqEuaCr9OLfq6czhban+i4cD3ZdIjehfuHqctzjuLw8Jw==} '@push.rocks/smartshell@3.3.0': resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==} @@ -861,6 +783,9 @@ packages: '@push.rocks/smartversion@3.0.5': resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==} + '@push.rocks/smartxml@1.1.1': + resolution: {integrity: sha512-1toSmLE1EGK8oENh09XjV588+IdzUB3x1PCaxKjSyIsAt54bUQj3kH/yzLODF+19p07OE0KM5U1oqWpjOcFCzA==} + '@push.rocks/smartxml@2.0.0': resolution: {integrity: sha512-1d06zYJX4Zt8s5w5qFOUg2LAEz9ykrh9d6CQPK4WAgOBIefb1xzVEWHc7yoxicc2OkzNgC3IBCEg3s6BncZKWw==} @@ -1293,6 +1218,9 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -1397,8 +1325,8 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@22.17.2': - resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} '@types/ping@0.4.4': resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} @@ -1734,20 +1662,27 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + color-name@1.1.3: resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} - color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} - colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} @@ -1840,15 +1775,6 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2200,6 +2126,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2371,9 +2301,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -2383,8 +2310,8 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-nan@1.3.2: @@ -2450,6 +2377,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -2492,8 +2423,8 @@ packages: resolution: {integrity: sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==} engines: {node: '>= 7.6.0'} - koa@2.16.2: - resolution: {integrity: sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==} + koa@2.16.3: + resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==} engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} kuler@2.0.0: @@ -2771,9 +2702,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} @@ -3084,12 +3015,12 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@24.30.0: - resolution: {integrity: sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==} + puppeteer-core@24.31.0: + resolution: {integrity: sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==} engines: {node: '>=18'} - puppeteer@24.30.0: - resolution: {integrity: sha512-A5OtCi9WpiXBQgJ2vQiZHSyrAzQmO/WDsvghqlN4kgw21PhxA5knHUaUQq/N3EMt8CcvSS0RM+kmYLJmedR3TQ==} + puppeteer@24.31.0: + resolution: {integrity: sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==} engines: {node: '>=18'} hasBin: true @@ -3274,9 +3205,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=} - smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -3576,8 +3504,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - webdriver-bidi-protocol@0.3.8: - resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==} + webdriver-bidi-protocol@0.3.9: + resolution: {integrity: sha512-uIYvlRQ0PwtZR1EzHlTMol1G0lAlmOe6wPykF9a77AK3bkpvZHzIVxRE2ThOx5vjy2zISe0zhwf5rzuUfbo1PQ==} webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} @@ -3605,8 +3533,8 @@ packages: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} - winston@3.17.0: - resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + winston@3.18.3: + resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} engines: {node: '>= 12.0.0'} wrap-ansi@7.0.0: @@ -3644,6 +3572,10 @@ packages: utf-8-validate: optional: true + xmlbuilder2@3.1.1: + resolution: {integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==} + engines: {node: '>=12.0'} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -3816,51 +3748,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-cognito-identity@3.864.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/credential-provider-node': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.4.3 - '@smithy/core': 3.18.5 - '@smithy/fetch-http-handler': 5.3.6 - '@smithy/hash-node': 4.2.5 - '@smithy/invalid-dependency': 4.2.5 - '@smithy/middleware-content-length': 4.2.5 - '@smithy/middleware-endpoint': 4.3.12 - '@smithy/middleware-retry': 4.4.12 - '@smithy/middleware-serde': 4.2.6 - '@smithy/middleware-stack': 4.2.5 - '@smithy/node-config-provider': 4.3.5 - '@smithy/node-http-handler': 4.4.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/smithy-client': 4.9.8 - '@smithy/types': 4.9.0 - '@smithy/url-parser': 4.2.5 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.11 - '@smithy/util-defaults-mode-node': 4.2.14 - '@smithy/util-endpoints': 3.2.5 - '@smithy/util-middleware': 4.2.5 - '@smithy/util-retry': 4.2.5 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/client-s3@3.936.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 @@ -3921,50 +3808,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.864.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.4.3 - '@smithy/core': 3.18.5 - '@smithy/fetch-http-handler': 5.3.6 - '@smithy/hash-node': 4.2.5 - '@smithy/invalid-dependency': 4.2.5 - '@smithy/middleware-content-length': 4.2.5 - '@smithy/middleware-endpoint': 4.3.12 - '@smithy/middleware-retry': 4.4.12 - '@smithy/middleware-serde': 4.2.6 - '@smithy/middleware-stack': 4.2.5 - '@smithy/node-config-provider': 4.3.5 - '@smithy/node-http-handler': 4.4.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/smithy-client': 4.9.8 - '@smithy/types': 4.9.0 - '@smithy/url-parser': 4.2.5 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.11 - '@smithy/util-defaults-mode-node': 4.2.14 - '@smithy/util-endpoints': 3.2.5 - '@smithy/util-middleware': 4.2.5 - '@smithy/util-retry': 4.2.5 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/client-sso@3.936.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -4008,25 +3851,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.864.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@aws-sdk/xml-builder': 3.862.0 - '@smithy/core': 3.18.5 - '@smithy/node-config-provider': 4.3.5 - '@smithy/property-provider': 4.2.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/signature-v4': 5.3.5 - '@smithy/smithy-client': 4.9.8 - '@smithy/types': 4.9.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-middleware': 4.2.5 - '@smithy/util-utf8': 4.2.0 - fast-xml-parser: 5.2.5 - tslib: 2.8.1 - optional: true - '@aws-sdk/core@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4043,26 +3867,6 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-cognito-identity@3.864.0': - dependencies: - '@aws-sdk/client-cognito-identity': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - - '@aws-sdk/credential-provider-env@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/credential-provider-env@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4071,20 +3875,6 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/fetch-http-handler': 5.3.6 - '@smithy/node-http-handler': 4.4.5 - '@smithy/property-provider': 4.2.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/smithy-client': 4.9.8 - '@smithy/types': 4.9.0 - '@smithy/util-stream': 4.5.6 - tslib: 2.8.1 - optional: true - '@aws-sdk/credential-provider-http@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4098,25 +3888,6 @@ snapshots: '@smithy/util-stream': 4.5.6 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/credential-provider-env': 3.864.0 - '@aws-sdk/credential-provider-http': 3.864.0 - '@aws-sdk/credential-provider-process': 3.864.0 - '@aws-sdk/credential-provider-sso': 3.864.0 - '@aws-sdk/credential-provider-web-identity': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.2.5 - '@smithy/property-provider': 4.2.5 - '@smithy/shared-ini-file-loader': 4.4.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/credential-provider-ini@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4149,24 +3920,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.864.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.864.0 - '@aws-sdk/credential-provider-http': 3.864.0 - '@aws-sdk/credential-provider-ini': 3.864.0 - '@aws-sdk/credential-provider-process': 3.864.0 - '@aws-sdk/credential-provider-sso': 3.864.0 - '@aws-sdk/credential-provider-web-identity': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.2.5 - '@smithy/property-provider': 4.2.5 - '@smithy/shared-ini-file-loader': 4.4.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/credential-provider-node@3.936.0': dependencies: '@aws-sdk/credential-provider-env': 3.936.0 @@ -4184,16 +3937,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/shared-ini-file-loader': 4.4.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/credential-provider-process@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4203,20 +3946,6 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.864.0': - dependencies: - '@aws-sdk/client-sso': 3.864.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/token-providers': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/shared-ini-file-loader': 4.4.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/credential-provider-sso@3.936.0': dependencies: '@aws-sdk/client-sso': 3.936.0 @@ -4230,18 +3959,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/credential-provider-web-identity@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4254,31 +3971,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-providers@3.864.0': - dependencies: - '@aws-sdk/client-cognito-identity': 3.864.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/credential-provider-cognito-identity': 3.864.0 - '@aws-sdk/credential-provider-env': 3.864.0 - '@aws-sdk/credential-provider-http': 3.864.0 - '@aws-sdk/credential-provider-ini': 3.864.0 - '@aws-sdk/credential-provider-node': 3.864.0 - '@aws-sdk/credential-provider-process': 3.864.0 - '@aws-sdk/credential-provider-sso': 3.864.0 - '@aws-sdk/credential-provider-web-identity': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/config-resolver': 4.4.3 - '@smithy/core': 3.18.5 - '@smithy/credential-provider-imds': 4.2.5 - '@smithy/node-config-provider': 4.3.5 - '@smithy/property-provider': 4.2.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/middleware-bucket-endpoint@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4312,14 +4004,6 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.3.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/middleware-host-header@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4333,31 +4017,16 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/middleware-logger@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.3.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/middleware-recursion-detection@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 - '@aws/lambda-invoke-store': 0.2.0 + '@aws/lambda-invoke-store': 0.2.1 '@smithy/protocol-http': 5.3.5 '@smithy/types': 4.9.0 tslib: 2.8.1 @@ -4385,17 +4054,6 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@smithy/core': 3.18.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/middleware-user-agent@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4406,50 +4064,6 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.864.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.4.3 - '@smithy/core': 3.18.5 - '@smithy/fetch-http-handler': 5.3.6 - '@smithy/hash-node': 4.2.5 - '@smithy/invalid-dependency': 4.2.5 - '@smithy/middleware-content-length': 4.2.5 - '@smithy/middleware-endpoint': 4.3.12 - '@smithy/middleware-retry': 4.4.12 - '@smithy/middleware-serde': 4.2.6 - '@smithy/middleware-stack': 4.2.5 - '@smithy/node-config-provider': 4.3.5 - '@smithy/node-http-handler': 4.4.5 - '@smithy/protocol-http': 5.3.5 - '@smithy/smithy-client': 4.9.8 - '@smithy/types': 4.9.0 - '@smithy/url-parser': 4.2.5 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.11 - '@smithy/util-defaults-mode-node': 4.2.14 - '@smithy/util-endpoints': 3.2.5 - '@smithy/util-middleware': 4.2.5 - '@smithy/util-retry': 4.2.5 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/nested-clients@3.936.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -4493,16 +4107,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.3.5 - '@smithy/types': 4.9.0 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-middleware': 4.2.5 - tslib: 2.8.1 - optional: true - '@aws-sdk/region-config-resolver@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4520,19 +4124,6 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.2.5 - '@smithy/shared-ini-file-loader': 4.4.0 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - '@aws-sdk/token-providers@3.936.0': dependencies: '@aws-sdk/core': 3.936.0 @@ -4545,12 +4136,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.862.0': - dependencies: - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/types@3.936.0': dependencies: '@smithy/types': 4.9.0 @@ -4560,15 +4145,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.9.0 - '@smithy/url-parser': 4.2.5 - '@smithy/util-endpoints': 3.2.5 - tslib: 2.8.1 - optional: true - '@aws-sdk/util-endpoints@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4581,14 +4157,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.862.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.9.0 - bowser: 2.12.1 - tslib: 2.8.1 - optional: true - '@aws-sdk/util-user-agent-browser@3.936.0': dependencies: '@aws-sdk/types': 3.936.0 @@ -4596,15 +4164,6 @@ snapshots: bowser: 2.12.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.864.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.3.5 - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/util-user-agent-node@3.936.0': dependencies: '@aws-sdk/middleware-user-agent': 3.936.0 @@ -4613,19 +4172,13 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.862.0': - dependencies: - '@smithy/types': 4.9.0 - tslib: 2.8.1 - optional: true - '@aws-sdk/xml-builder@3.930.0': dependencies: '@smithy/types': 4.9.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.0': {} + '@aws/lambda-invoke-store@0.2.1': {} '@babel/code-frame@7.27.1': dependencies: @@ -4647,9 +4200,9 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@dabh/diagnostics@2.0.3': + '@dabh/diagnostics@2.0.8': dependencies: - colorspace: 1.1.4 + '@so-ric/colorspace': 1.1.6 enabled: 2.0.0 kuler: 2.0.0 @@ -4859,7 +4412,7 @@ snapshots: '@push.rocks/smartshell': 3.3.0 tsx: 4.20.6 - '@git.zone/tstest@3.0.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.9.3)': + '@git.zone/tstest@3.0.1(socks@2.8.7)(typescript@5.9.3)': dependencies: '@api.global/typedserver': 3.0.80 '@git.zone/tsbundle': 2.5.2 @@ -4875,12 +4428,12 @@ snapshots: '@push.rocks/smartfile': 11.2.7 '@push.rocks/smartjson': 5.2.0 '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartmongo': 2.0.14(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + '@push.rocks/smartmongo': 2.0.14(socks@2.8.7) '@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smarts3': 2.2.6 + '@push.rocks/smarts3': 2.2.7 '@push.rocks/smartshell': 3.3.0 '@push.rocks/smarttime': 4.1.1 '@types/ws': 8.18.1 @@ -4928,7 +4481,7 @@ snapshots: '@koa/router@9.4.0': dependencies: - debug: 4.4.1 + debug: 4.4.3 http-errors: 1.8.1 koa-compose: 4.1.0 methods: 1.1.2 @@ -4982,6 +4535,23 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@oozcitak/dom@1.15.10': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/url': 1.0.4 + '@oozcitak/util': 8.3.8 + + '@oozcitak/infra@1.0.8': + dependencies: + '@oozcitak/util': 8.3.8 + + '@oozcitak/url@1.0.4': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + + '@oozcitak/util@8.3.8': {} + '@oxc-project/types@0.98.0': {} '@pdf-lib/standard-fonts@1.0.0': @@ -5158,7 +4728,7 @@ snapshots: '@types/symbol-tree': 3.2.5 symbol-tree: 3.2.4 - '@push.rocks/mongodump@1.1.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)': + '@push.rocks/mongodump@1.1.0(socks@2.8.7)': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartfile': 11.2.7 @@ -5166,7 +4736,7 @@ snapshots: '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 '@tsclass/tsclass': 9.3.0 - mongodb: 6.21.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + mongodb: 6.21.0(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -5278,12 +4848,12 @@ snapshots: '@types/node-forge': 1.3.14 node-forge: 1.3.1 - '@push.rocks/smartdata@5.16.7(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)': + '@push.rocks/smartdata@5.16.7(socks@2.8.7)': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartmongo': 2.0.14(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + '@push.rocks/smartmongo': 2.0.14(socks@2.8.7) '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 '@push.rocks/smartstring': 4.1.0 @@ -5291,7 +4861,7 @@ snapshots: '@push.rocks/smartunique': 3.0.9 '@push.rocks/taskbuffer': 3.4.0 '@tsclass/tsclass': 9.3.0 - mongodb: 6.21.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + mongodb: 6.21.0(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -5453,13 +5023,13 @@ snapshots: file-type: 19.6.0 mime: 4.1.0 - '@push.rocks/smartmongo@2.0.14(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)': + '@push.rocks/smartmongo@2.0.14(socks@2.8.7)': dependencies: - '@push.rocks/mongodump': 1.1.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) - '@push.rocks/smartdata': 5.16.7(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + '@push.rocks/mongodump': 1.1.0(socks@2.8.7) + '@push.rocks/smartdata': 5.16.7(socks@2.8.7) '@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpromise': 4.2.3 - mongodb-memory-server: 10.3.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + mongodb-memory-server: 10.3.0(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -5567,7 +5137,7 @@ snapshots: dependencies: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartshell': 3.3.0 - puppeteer: 24.30.0(typescript@5.9.3) + puppeteer: 24.31.0(typescript@5.9.3) tree-kill: 1.2.2 transitivePeerDependencies: - bare-abort-controller @@ -5614,7 +5184,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 rxjs: 7.8.2 - '@push.rocks/smarts3@2.2.6': + '@push.rocks/smarts3@2.2.7': dependencies: '@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartfile': 11.2.7 @@ -5722,6 +5292,11 @@ snapshots: '@types/semver': 7.7.1 semver: 7.7.3 + '@push.rocks/smartxml@1.1.1': + dependencies: + fast-xml-parser: 4.5.3 + xmlbuilder2: 3.1.1 + '@push.rocks/smartxml@2.0.0': dependencies: fast-xml-parser: 5.3.2 @@ -6271,6 +5846,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@socket.io/component-emitter@3.1.2': {} '@szmarczak/http-timer@5.0.1': @@ -6305,27 +5885,27 @@ snapshots: '@types/bn.js@5.2.0': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/buffer-json@2.0.3': {} '@types/clean-css@4.2.11': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 source-map: 0.6.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/cors@2.8.19': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/debug@4.1.12': dependencies: @@ -6333,7 +5913,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/elliptic@6.4.18': dependencies: @@ -6341,7 +5921,7 @@ snapshots: '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -6355,7 +5935,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/hast@3.0.4': dependencies: @@ -6377,7 +5957,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/mdast@4.0.4': dependencies: @@ -6393,9 +5973,9 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 - '@types/node@22.17.2': + '@types/node@22.19.1': dependencies: undici-types: 6.21.0 @@ -6411,34 +5991,34 @@ snapshots: '@types/s3rver@3.7.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/semver@7.7.1': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/send@1.2.1': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/send': 0.17.6 '@types/symbol-tree@3.2.5': {} '@types/tar-stream@3.1.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/through2@2.0.41': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/triple-beam@1.3.5': {} @@ -6464,11 +6044,11 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.19.1 optional: true '@ungap/structured-clone@1.3.0': {} @@ -6480,7 +6060,7 @@ snapshots: accepts@2.0.0: dependencies: - mime-types: 3.0.1 + mime-types: 3.0.2 negotiator: 1.0.0 acme-client@5.4.0: @@ -6738,24 +6318,24 @@ snapshots: dependencies: color-name: 1.1.4 + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + color-name@1.1.3: {} color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 + color-name@2.1.0: {} - color@3.2.1: + color-string@2.1.4: dependencies: - color-convert: 1.9.3 - color-string: 1.9.1 + color-name: 2.1.0 - colorspace@1.1.4: + color@5.0.3: dependencies: - color: 3.2.1 - text-hex: 1.0.0 + color-convert: 3.1.3 + color-string: 2.1.4 combined-stream@1.0.8: dependencies: @@ -6829,10 +6409,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -6948,7 +6524,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 22.17.2 + '@types/node': 22.19.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -7067,7 +6643,7 @@ snapshots: fresh: 2.0.0 http-errors: 2.0.0 merge-descriptors: 2.0.0 - mime-types: 3.0.1 + mime-types: 3.0.2 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 @@ -7228,6 +6804,8 @@ snapshots: function-bind@1.1.2: {} + generator-function@2.0.1: {} + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -7458,15 +7036,14 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} - is-docker@2.2.1: {} is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -7517,6 +7094,11 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -7564,14 +7146,14 @@ snapshots: humanize-number: 0.0.2 passthrough-counter: 1.0.0 - koa@2.16.2: + koa@2.16.3: dependencies: accepts: 1.3.8 cache-content-type: 1.0.1 content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.9.1 - debug: 4.4.1 + debug: 4.4.3 delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -7580,7 +7162,7 @@ snapshots: fresh: 0.5.2 http-assert: 1.5.0 http-errors: 1.8.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 koa-compose: 4.1.0 koa-convert: 2.0.0 on-finished: 2.4.1 @@ -8039,7 +7621,7 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -8076,7 +7658,7 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 - mongodb-memory-server-core@10.3.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7): + mongodb-memory-server-core@10.3.0(socks@2.8.7): dependencies: async-mutex: 0.5.0 camelcase: 6.3.0 @@ -8084,7 +7666,7 @@ snapshots: find-cache-dir: 3.3.2 follow-redirects: 1.15.11(debug@4.4.3) https-proxy-agent: 7.0.6 - mongodb: 6.21.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + mongodb: 6.21.0(socks@2.8.7) new-find-package-json: 2.0.0 semver: 7.7.3 tar-stream: 3.1.7 @@ -8102,9 +7684,9 @@ snapshots: - socks - supports-color - mongodb-memory-server@10.3.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7): + mongodb-memory-server@10.3.0(socks@2.8.7): dependencies: - mongodb-memory-server-core: 10.3.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7) + mongodb-memory-server-core: 10.3.0(socks@2.8.7) tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/credential-providers' @@ -8118,13 +7700,12 @@ snapshots: - socks - supports-color - mongodb@6.21.0(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7): + mongodb@6.21.0(socks@2.8.7): dependencies: '@mongodb-js/saslprep': 1.3.2 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 optionalDependencies: - '@aws-sdk/credential-providers': 3.864.0 socks: 2.8.7 ms@2.1.3: {} @@ -8335,14 +7916,14 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@24.30.0: + puppeteer-core@24.31.0: dependencies: '@puppeteer/browsers': 2.10.13 chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046) debug: 4.4.3 devtools-protocol: 0.0.1521046 typed-query-selector: 2.12.0 - webdriver-bidi-protocol: 0.3.8 + webdriver-bidi-protocol: 0.3.9 ws: 8.18.3 transitivePeerDependencies: - bare-abort-controller @@ -8352,13 +7933,13 @@ snapshots: - supports-color - utf-8-validate - puppeteer@24.30.0(typescript@5.9.3): + puppeteer@24.31.0(typescript@5.9.3): dependencies: '@puppeteer/browsers': 2.10.13 chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046) cosmiconfig: 9.0.0(typescript@5.9.3) devtools-protocol: 0.0.1521046 - puppeteer-core: 24.30.0 + puppeteer-core: 24.31.0 typed-query-selector: 2.12.0 transitivePeerDependencies: - bare-abort-controller @@ -8526,11 +8107,11 @@ snapshots: fast-xml-parser: 3.21.1 fs-extra: 8.1.0 he: 1.2.0 - koa: 2.16.2 + koa: 2.16.3 koa-logger: 3.2.1 lodash: 4.17.21 statuses: 2.0.2 - winston: 3.17.0 + winston: 3.18.3 transitivePeerDependencies: - supports-color @@ -8558,7 +8139,7 @@ snapshots: etag: 1.8.1 fresh: 2.0.0 http-errors: 2.0.0 - mime-types: 3.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -8624,10 +8205,6 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - smart-buffer@4.2.0: {} socket.io-adapter@2.5.5: @@ -8884,7 +8461,7 @@ snapshots: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.1 + mime-types: 3.0.2 typed-query-selector@2.12.0: {} @@ -8962,7 +8539,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - webdriver-bidi-protocol@0.3.8: {} + webdriver-bidi-protocol@0.3.9: {} webidl-conversions@7.0.0: {} @@ -8987,10 +8564,10 @@ snapshots: readable-stream: 3.6.2 triple-beam: 1.4.1 - winston@3.17.0: + winston@3.18.3: dependencies: '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.3 + '@dabh/diagnostics': 2.0.8 async: 3.2.6 is-stream: 2.0.1 logform: 2.7.0 @@ -9019,6 +8596,13 @@ snapshots: ws@8.18.3: {} + xmlbuilder2@3.1.1: + dependencies: + '@oozcitak/dom': 1.15.10 + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + js-yaml: 3.14.1 + xmlhttprequest-ssl@2.1.2: {} y18n@5.0.8: {} diff --git a/readme.md b/readme.md index 250cadd..b9f1be6 100644 --- a/readme.md +++ b/readme.md @@ -5,12 +5,14 @@ ## ๐ŸŒŸ Features - ๐Ÿƒ **Lightning-fast local S3 simulation** - No more waiting for cloud operations during development -- ๐Ÿ”„ **Full AWS S3 API compatibility** - Drop-in replacement for S3 in your tests -- ๐Ÿ“‚ **Local directory mapping** - Your buckets live right on your filesystem +- โšก **Native custom S3 server** - Built on Node.js http module with zero framework dependencies (default) +- ๐Ÿ”„ **Full AWS S3 API compatibility** - Drop-in replacement for AWS SDK v3 and other S3 clients +- ๐Ÿ“‚ **Local directory mapping** - Your buckets live right on your filesystem with Windows-compatible encoding - ๐Ÿงช **Perfect for testing** - Reliable, repeatable tests without cloud dependencies - ๐ŸŽฏ **TypeScript-first** - Built with TypeScript for excellent type safety and IDE support - ๐Ÿ”ง **Zero configuration** - Works out of the box with sensible defaults - ๐Ÿงน **Clean slate mode** - Start fresh on every test run +- ๐Ÿ”€ **Legacy compatibility** - Optional s3rver backend support for backward compatibility ## ๐Ÿ“ฆ Installation @@ -410,7 +412,7 @@ interface ISmarts3ContructorOptions { - [`@push.rocks/smartbucket`](https://www.npmjs.com/package/@push.rocks/smartbucket) - Powerful S3 abstraction layer - [`@push.rocks/smartfile`](https://www.npmjs.com/package/@push.rocks/smartfile) - Advanced file system operations - [`@tsclass/tsclass`](https://www.npmjs.com/package/@tsclass/tsclass) - TypeScript class helpers -- [`s3rver`](https://www.npmjs.com/package/s3rver) - The underlying S3 server implementation +- [`s3rver`](https://www.npmjs.com/package/s3rver) - Optional legacy S3 server implementation (used when `useCustomServer: false`) ## License and Legal Information diff --git a/test/test.aws-sdk.node.ts b/test/test.aws-sdk.node.ts new file mode 100644 index 0000000..d3dead6 --- /dev/null +++ b/test/test.aws-sdk.node.ts @@ -0,0 +1,104 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { S3Client, CreateBucketCommand, ListBucketsCommand, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteBucketCommand } from '@aws-sdk/client-s3'; +import { Readable } from 'stream'; +import * as smarts3 from '../ts/index.js'; + +let testSmarts3Instance: smarts3.Smarts3; +let s3Client: S3Client; + +// Helper to convert stream to string +async function streamToString(stream: Readable): Promise { + const chunks: Buffer[] = []; + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + }); +} + +tap.test('should start the S3 server and configure client', async () => { + testSmarts3Instance = await smarts3.Smarts3.createAndStart({ + port: 3337, + cleanSlate: true, + silent: true, + }); + + const descriptor = await testSmarts3Instance.getS3Descriptor(); + + s3Client = new S3Client({ + endpoint: `http://${descriptor.endpoint}:${descriptor.port}`, + region: 'us-east-1', + credentials: { + accessKeyId: descriptor.accessKey, + secretAccessKey: descriptor.accessSecret, + }, + forcePathStyle: true, + }); +}); + +tap.test('should list buckets (empty)', async () => { + const response = await s3Client.send(new ListBucketsCommand({})); + expect(Array.isArray(response.Buckets)).toEqual(true); + expect(response.Buckets!.length).toEqual(0); +}); + +tap.test('should create a bucket', async () => { + const response = await s3Client.send(new CreateBucketCommand({ Bucket: 'test-bucket' })); + expect(response.$metadata.httpStatusCode).toEqual(200); +}); + +tap.test('should list buckets (showing created bucket)', async () => { + const response = await s3Client.send(new ListBucketsCommand({})); + expect(response.Buckets!.length).toEqual(1); + expect(response.Buckets![0].Name).toEqual('test-bucket'); +}); + +tap.test('should upload an object', async () => { + const response = await s3Client.send(new PutObjectCommand({ + Bucket: 'test-bucket', + Key: 'test-file.txt', + Body: 'Hello from AWS SDK!', + ContentType: 'text/plain', + })); + expect(response.$metadata.httpStatusCode).toEqual(200); + expect(response.ETag).toBeTypeofString(); +}); + +tap.test('should download the object', async () => { + const response = await s3Client.send(new GetObjectCommand({ + Bucket: 'test-bucket', + Key: 'test-file.txt', + })); + + expect(response.$metadata.httpStatusCode).toEqual(200); + const content = await streamToString(response.Body as Readable); + expect(content).toEqual('Hello from AWS SDK!'); +}); + +tap.test('should delete the object', async () => { + const response = await s3Client.send(new DeleteObjectCommand({ + Bucket: 'test-bucket', + Key: 'test-file.txt', + })); + expect(response.$metadata.httpStatusCode).toEqual(204); +}); + +tap.test('should fail to get deleted object', async () => { + await expect( + s3Client.send(new GetObjectCommand({ + Bucket: 'test-bucket', + Key: 'test-file.txt', + })) + ).rejects.toThrow(); +}); + +tap.test('should delete the bucket', async () => { + const response = await s3Client.send(new DeleteBucketCommand({ Bucket: 'test-bucket' })); + expect(response.$metadata.httpStatusCode).toEqual(204); +}); + +tap.test('should stop the S3 server', async () => { + await testSmarts3Instance.stop(); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 96d43a7..cab73e8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smarts3', - version: '2.2.7', + version: '2.3.0', description: 'A Node.js TypeScript package to create a local S3 endpoint for simulating AWS S3 operations using mapped local directories for development and testing purposes.' } diff --git a/ts/classes/context.ts b/ts/classes/context.ts new file mode 100644 index 0000000..43b38fd --- /dev/null +++ b/ts/classes/context.ts @@ -0,0 +1,114 @@ +import * as plugins from '../plugins.js'; +import { S3Error } from './s3-error.js'; +import { createXml } from '../utils/xml.utils.js'; +import type { FilesystemStore } from './filesystem-store.js'; +import type { Readable } from 'stream'; + +/** + * S3 request context with helper methods + */ +export class S3Context { + public method: string; + public url: URL; + public headers: plugins.http.IncomingHttpHeaders; + public params: Record = {}; + public query: Record = {}; + public store: FilesystemStore; + + private req: plugins.http.IncomingMessage; + private res: plugins.http.ServerResponse; + private statusCode: number = 200; + private responseHeaders: Record = {}; + + constructor( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + store: FilesystemStore + ) { + this.req = req; + this.res = res; + this.store = store; + this.method = req.method || 'GET'; + this.headers = req.headers; + + // Parse URL and query string + const fullUrl = `http://${req.headers.host || 'localhost'}${req.url || '/'}`; + this.url = new URL(fullUrl); + + // Parse query string into object + this.url.searchParams.forEach((value, key) => { + this.query[key] = value; + }); + } + + /** + * Set response status code + */ + public status(code: number): this { + this.statusCode = code; + return this; + } + + /** + * Set response header + */ + public setHeader(name: string, value: string | number): this { + this.responseHeaders[name] = value.toString(); + return this; + } + + /** + * Send response body (string, Buffer, or Stream) + */ + public async send(body: string | Buffer | Readable | NodeJS.ReadableStream): Promise { + // Write status and headers + this.res.writeHead(this.statusCode, this.responseHeaders); + + // Handle different body types + if (typeof body === 'string' || body instanceof Buffer) { + this.res.end(body); + } else if (body && typeof (body as any).pipe === 'function') { + // It's a stream + (body as Readable).pipe(this.res); + } else { + this.res.end(); + } + } + + /** + * Send XML response + */ + public async sendXML(obj: any): Promise { + const xml = createXml(obj, { format: true }); + this.setHeader('Content-Type', 'application/xml'); + this.setHeader('Content-Length', Buffer.byteLength(xml)); + await this.send(xml); + } + + /** + * Throw an S3 error + */ + public throw(code: string, message: string, detail?: Record): never { + throw new S3Error(code, message, detail); + } + + /** + * Read and parse request body as string + */ + public async readBody(): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + + this.req.on('data', (chunk) => chunks.push(chunk)); + this.req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + this.req.on('error', reject); + }); + } + + /** + * Get the request stream (for streaming uploads) + */ + public getRequestStream(): NodeJS.ReadableStream { + return this.req; + } +} diff --git a/ts/classes/filesystem-store.ts b/ts/classes/filesystem-store.ts new file mode 100644 index 0000000..982dfe2 --- /dev/null +++ b/ts/classes/filesystem-store.ts @@ -0,0 +1,495 @@ +import * as plugins from '../plugins.js'; +import { S3Error } from './s3-error.js'; +import type { Readable } from 'stream'; + +export interface IS3Bucket { + name: string; + creationDate: Date; +} + +export interface IS3Object { + key: string; + size: number; + lastModified: Date; + md5: string; + metadata: Record; + content?: Readable; +} + +export interface IListObjectsOptions { + prefix?: string; + delimiter?: string; + maxKeys?: number; + continuationToken?: string; +} + +export interface IListObjectsResult { + contents: IS3Object[]; + commonPrefixes: string[]; + isTruncated: boolean; + nextContinuationToken?: string; + prefix: string; + delimiter: string; + maxKeys: number; +} + +export interface IRangeOptions { + start: number; + end: number; +} + +/** + * Filesystem-backed storage for S3 objects + */ +export class FilesystemStore { + constructor(private rootDir: string) {} + + /** + * Initialize store (ensure root directory exists) + */ + public async initialize(): Promise { + await plugins.fs.promises.mkdir(this.rootDir, { recursive: true }); + } + + /** + * Reset store (delete all buckets) + */ + public async reset(): Promise { + await plugins.smartfile.fs.ensureEmptyDir(this.rootDir); + } + + // ============================ + // BUCKET OPERATIONS + // ============================ + + /** + * List all buckets + */ + public async listBuckets(): Promise { + const dirs = await plugins.smartfile.fs.listFolders(this.rootDir); + const buckets: IS3Bucket[] = []; + + for (const dir of dirs) { + const bucketPath = plugins.path.join(this.rootDir, dir); + const stats = await plugins.smartfile.fs.stat(bucketPath); + + buckets.push({ + name: dir, + creationDate: stats.birthtime, + }); + } + + return buckets.sort((a, b) => a.name.localeCompare(b.name)); + } + + /** + * Check if bucket exists + */ + public async bucketExists(bucket: string): Promise { + const bucketPath = this.getBucketPath(bucket); + return plugins.smartfile.fs.isDirectory(bucketPath); + } + + /** + * Create bucket + */ + public async createBucket(bucket: string): Promise { + const bucketPath = this.getBucketPath(bucket); + await plugins.fs.promises.mkdir(bucketPath, { recursive: true }); + } + + /** + * Delete bucket (must be empty) + */ + public async deleteBucket(bucket: string): Promise { + const bucketPath = this.getBucketPath(bucket); + + // Check if bucket exists + if (!(await this.bucketExists(bucket))) { + throw new S3Error('NoSuchBucket', 'The specified bucket does not exist'); + } + + // Check if bucket is empty + const files = await plugins.smartfile.fs.listFileTree(bucketPath, '**/*'); + if (files.length > 0) { + throw new S3Error('BucketNotEmpty', 'The bucket you tried to delete is not empty'); + } + + await plugins.smartfile.fs.remove(bucketPath); + } + + // ============================ + // OBJECT OPERATIONS + // ============================ + + /** + * List objects in bucket + */ + public async listObjects( + bucket: string, + options: IListObjectsOptions = {} + ): Promise { + const bucketPath = this.getBucketPath(bucket); + + if (!(await this.bucketExists(bucket))) { + throw new S3Error('NoSuchBucket', 'The specified bucket does not exist'); + } + + const { + prefix = '', + delimiter = '', + maxKeys = 1000, + continuationToken, + } = options; + + // List all object files + const objectPattern = '**/*._S3_object'; + const objectFiles = await plugins.smartfile.fs.listFileTree(bucketPath, objectPattern); + + // Convert file paths to keys + let keys = objectFiles.map((filePath) => { + const relativePath = plugins.path.relative(bucketPath, filePath); + const key = this.decodeKey(relativePath.replace(/\._S3_object$/, '')); + return key; + }); + + // Apply prefix filter + if (prefix) { + keys = keys.filter((key) => key.startsWith(prefix)); + } + + // Sort keys + keys = keys.sort(); + + // Handle continuation token (simple implementation using key name) + if (continuationToken) { + const startIndex = keys.findIndex((key) => key > continuationToken); + if (startIndex > 0) { + keys = keys.slice(startIndex); + } + } + + // Handle delimiter (common prefixes) + const commonPrefixes: Set = new Set(); + const contents: IS3Object[] = []; + + for (const key of keys) { + if (delimiter) { + // Find first delimiter after prefix + const remainingKey = key.slice(prefix.length); + const delimiterIndex = remainingKey.indexOf(delimiter); + + if (delimiterIndex !== -1) { + // This key has a delimiter, add to common prefixes + const commonPrefix = prefix + remainingKey.slice(0, delimiterIndex + delimiter.length); + commonPrefixes.add(commonPrefix); + continue; + } + } + + // Add to contents (limited by maxKeys) + if (contents.length >= maxKeys) { + break; + } + + try { + const objectInfo = await this.getObjectInfo(bucket, key); + contents.push(objectInfo); + } catch (err) { + // Skip if object no longer exists + continue; + } + } + + const isTruncated = keys.length > contents.length + commonPrefixes.size; + const nextContinuationToken = isTruncated + ? contents[contents.length - 1]?.key + : undefined; + + return { + contents, + commonPrefixes: Array.from(commonPrefixes).sort(), + isTruncated, + nextContinuationToken, + prefix, + delimiter, + maxKeys, + }; + } + + /** + * Get object info (without content) + */ + private async getObjectInfo(bucket: string, key: string): Promise { + const objectPath = this.getObjectPath(bucket, key); + const metadataPath = `${objectPath}.metadata.json`; + const md5Path = `${objectPath}.md5`; + + const [stats, metadata, md5] = await Promise.all([ + plugins.smartfile.fs.stat(objectPath), + this.readMetadata(metadataPath), + this.readMD5(objectPath, md5Path), + ]); + + return { + key, + size: stats.size, + lastModified: stats.mtime, + md5, + metadata, + }; + } + + /** + * Check if object exists + */ + public async objectExists(bucket: string, key: string): Promise { + const objectPath = this.getObjectPath(bucket, key); + return plugins.smartfile.fs.fileExists(objectPath); + } + + /** + * Put object (upload with streaming) + */ + public async putObject( + bucket: string, + key: string, + stream: NodeJS.ReadableStream, + metadata: Record = {} + ): Promise<{ size: number; md5: string }> { + const objectPath = this.getObjectPath(bucket, key); + + // Ensure bucket exists + if (!(await this.bucketExists(bucket))) { + throw new S3Error('NoSuchBucket', 'The specified bucket does not exist'); + } + + // Ensure parent directory exists + await plugins.fs.promises.mkdir(plugins.path.dirname(objectPath), { recursive: true }); + + // Write with MD5 calculation + const result = await this.writeStreamWithMD5(stream, objectPath); + + // Save metadata + const metadataPath = `${objectPath}.metadata.json`; + await plugins.fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); + + return result; + } + + /** + * Get object (download with streaming) + */ + public async getObject( + bucket: string, + key: string, + range?: IRangeOptions + ): Promise { + const objectPath = this.getObjectPath(bucket, key); + + if (!(await this.objectExists(bucket, key))) { + throw new S3Error('NoSuchKey', 'The specified key does not exist'); + } + + const info = await this.getObjectInfo(bucket, key); + + // Create read stream with optional range (using native fs for range support) + const stream = range + ? plugins.fs.createReadStream(objectPath, { start: range.start, end: range.end }) + : plugins.fs.createReadStream(objectPath); + + return { + ...info, + content: stream, + }; + } + + /** + * Delete object + */ + public async deleteObject(bucket: string, key: string): Promise { + const objectPath = this.getObjectPath(bucket, key); + const metadataPath = `${objectPath}.metadata.json`; + const md5Path = `${objectPath}.md5`; + + // S3 doesn't throw error if object doesn't exist + await Promise.all([ + plugins.smartfile.fs.remove(objectPath).catch(() => {}), + plugins.smartfile.fs.remove(metadataPath).catch(() => {}), + plugins.smartfile.fs.remove(md5Path).catch(() => {}), + ]); + } + + /** + * Copy object + */ + public async copyObject( + srcBucket: string, + srcKey: string, + destBucket: string, + destKey: string, + metadataDirective: 'COPY' | 'REPLACE' = 'COPY', + newMetadata?: Record + ): Promise<{ size: number; md5: string }> { + const srcObjectPath = this.getObjectPath(srcBucket, srcKey); + const destObjectPath = this.getObjectPath(destBucket, destKey); + + // Check source exists + if (!(await this.objectExists(srcBucket, srcKey))) { + throw new S3Error('NoSuchKey', 'The specified key does not exist'); + } + + // Ensure dest bucket exists + if (!(await this.bucketExists(destBucket))) { + throw new S3Error('NoSuchBucket', 'The specified bucket does not exist'); + } + + // Ensure parent directory exists + await plugins.fs.promises.mkdir(plugins.path.dirname(destObjectPath), { recursive: true }); + + // Copy object file + await plugins.smartfile.fs.copy(srcObjectPath, destObjectPath); + + // Handle metadata + if (metadataDirective === 'COPY') { + // Copy metadata + const srcMetadataPath = `${srcObjectPath}.metadata.json`; + const destMetadataPath = `${destObjectPath}.metadata.json`; + await plugins.smartfile.fs.copy(srcMetadataPath, destMetadataPath).catch(() => {}); + } else if (newMetadata) { + // Replace with new metadata + const destMetadataPath = `${destObjectPath}.metadata.json`; + await plugins.fs.promises.writeFile(destMetadataPath, JSON.stringify(newMetadata, null, 2)); + } + + // Copy MD5 + const srcMD5Path = `${srcObjectPath}.md5`; + const destMD5Path = `${destObjectPath}.md5`; + await plugins.smartfile.fs.copy(srcMD5Path, destMD5Path).catch(() => {}); + + // Get result info + const stats = await plugins.smartfile.fs.stat(destObjectPath); + const md5 = await this.readMD5(destObjectPath, destMD5Path); + + return { size: stats.size, md5 }; + } + + // ============================ + // HELPER METHODS + // ============================ + + /** + * Get bucket directory path + */ + private getBucketPath(bucket: string): string { + return plugins.path.join(this.rootDir, bucket); + } + + /** + * Get object file path + */ + private getObjectPath(bucket: string, key: string): string { + return plugins.path.join( + this.rootDir, + bucket, + this.encodeKey(key) + '._S3_object' + ); + } + + /** + * Encode key for Windows compatibility + */ + private encodeKey(key: string): string { + if (process.platform === 'win32') { + // Replace invalid Windows filename chars with hex encoding + return key.replace(/[<>:"\\|?*]/g, (ch) => + '&' + Buffer.from(ch, 'utf8').toString('hex') + ); + } + return key; + } + + /** + * Decode key from filesystem path + */ + private decodeKey(encodedKey: string): string { + if (process.platform === 'win32') { + // Decode hex-encoded chars + return encodedKey.replace(/&([0-9a-f]{2})/gi, (_, hex) => + Buffer.from(hex, 'hex').toString('utf8') + ); + } + return encodedKey; + } + + /** + * Write stream to file with MD5 calculation + */ + private async writeStreamWithMD5( + input: NodeJS.ReadableStream, + destPath: string + ): Promise<{ size: number; md5: string }> { + const hash = plugins.crypto.createHash('md5'); + let totalSize = 0; + + return new Promise((resolve, reject) => { + const output = plugins.fs.createWriteStream(destPath); + + input.on('data', (chunk: Buffer) => { + hash.update(chunk); + totalSize += chunk.length; + }); + + input.on('error', reject); + output.on('error', reject); + + input.pipe(output).on('finish', async () => { + const md5 = hash.digest('hex'); + + // Save MD5 to separate file + const md5Path = `${destPath}.md5`; + await plugins.fs.promises.writeFile(md5Path, md5); + + resolve({ size: totalSize, md5 }); + }); + }); + } + + /** + * Read MD5 hash (calculate if missing) + */ + private async readMD5(objectPath: string, md5Path: string): Promise { + try { + // Try to read cached MD5 + const md5 = await plugins.smartfile.fs.toStringSync(md5Path); + return md5.trim(); + } catch (err) { + // Calculate MD5 if not cached + return new Promise((resolve, reject) => { + const hash = plugins.crypto.createHash('md5'); + const stream = plugins.fs.createReadStream(objectPath); + + stream.on('data', (chunk: Buffer) => hash.update(chunk)); + stream.on('end', async () => { + const md5 = hash.digest('hex'); + // Cache it + await plugins.fs.promises.writeFile(md5Path, md5); + resolve(md5); + }); + stream.on('error', reject); + }); + } + } + + /** + * Read metadata from JSON file + */ + private async readMetadata(metadataPath: string): Promise> { + try { + const content = await plugins.smartfile.fs.toStringSync(metadataPath); + return JSON.parse(content); + } catch (err) { + return {}; + } + } +} diff --git a/ts/classes/middleware-stack.ts b/ts/classes/middleware-stack.ts new file mode 100644 index 0000000..ac91474 --- /dev/null +++ b/ts/classes/middleware-stack.ts @@ -0,0 +1,43 @@ +import * as plugins from '../plugins.js'; +import type { S3Context } from './context.js'; + +export type Middleware = ( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + next: () => Promise +) => Promise; + +/** + * Middleware stack for composing request handlers + */ +export class MiddlewareStack { + private middlewares: Middleware[] = []; + + /** + * Add middleware to the stack + */ + public use(middleware: Middleware): void { + this.middlewares.push(middleware); + } + + /** + * Execute all middlewares in order + */ + public async execute( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context + ): Promise { + let index = 0; + + const next = async (): Promise => { + if (index < this.middlewares.length) { + const middleware = this.middlewares[index++]; + await middleware(req, res, ctx, next); + } + }; + + await next(); + } +} diff --git a/ts/classes/router.ts b/ts/classes/router.ts new file mode 100644 index 0000000..b3e930b --- /dev/null +++ b/ts/classes/router.ts @@ -0,0 +1,129 @@ +import * as plugins from '../plugins.js'; +import type { S3Context } from './context.js'; + +export type RouteHandler = ( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record +) => Promise; + +export interface IRouteMatch { + handler: RouteHandler; + params: Record; +} + +interface IRoute { + method: string; + pattern: RegExp; + paramNames: string[]; + handler: RouteHandler; +} + +/** + * Simple HTTP router with pattern matching for S3 routes + */ +export class S3Router { + private routes: IRoute[] = []; + + /** + * Add a route with pattern matching + * Supports patterns like: + * - "/" (exact match) + * - "/:bucket" (single param) + * - "/:bucket/:key*" (param with wildcard - captures everything after) + */ + public add(method: string, pattern: string, handler: RouteHandler): void { + const { regex, paramNames } = this.convertPatternToRegex(pattern); + + this.routes.push({ + method: method.toUpperCase(), + pattern: regex, + paramNames, + handler, + }); + } + + /** + * Match a request to a route + */ + public match(method: string, pathname: string): IRouteMatch | null { + // Normalize pathname: remove trailing slash unless it's root + const normalizedPath = pathname === '/' ? pathname : pathname.replace(/\/$/, ''); + + for (const route of this.routes) { + if (route.method !== method.toUpperCase()) { + continue; + } + + const match = normalizedPath.match(route.pattern); + if (match) { + // Extract params from captured groups + const params: Record = {}; + for (let i = 0; i < route.paramNames.length; i++) { + params[route.paramNames[i]] = decodeURIComponent(match[i + 1] || ''); + } + + return { + handler: route.handler, + params, + }; + } + } + + return null; + } + + /** + * Convert path pattern to RegExp + * Examples: + * - "/" โ†’ /^\/$/ + * - "/:bucket" โ†’ /^\/([^/]+)$/ + * - "/:bucket/:key*" โ†’ /^\/([^/]+)\/(.+)$/ + */ + private convertPatternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } { + const paramNames: string[] = []; + let regexStr = pattern; + + // Process all params in a single pass to maintain order + regexStr = regexStr.replace(/:(\w+)(\*)?/g, (match, paramName, isWildcard) => { + paramNames.push(paramName); + // :param* captures rest of path, :param captures single segment + return isWildcard ? '(.+)' : '([^/]+)'; + }); + + // Escape special regex characters + regexStr = regexStr.replace(/\//g, '\\/'); + + // Add anchors + regexStr = `^${regexStr}$`; + + return { + regex: new RegExp(regexStr), + paramNames, + }; + } + + /** + * Convenience methods for common HTTP methods + */ + public get(pattern: string, handler: RouteHandler): void { + this.add('GET', pattern, handler); + } + + public put(pattern: string, handler: RouteHandler): void { + this.add('PUT', pattern, handler); + } + + public post(pattern: string, handler: RouteHandler): void { + this.add('POST', pattern, handler); + } + + public delete(pattern: string, handler: RouteHandler): void { + this.add('DELETE', pattern, handler); + } + + public head(pattern: string, handler: RouteHandler): void { + this.add('HEAD', pattern, handler); + } +} diff --git a/ts/classes/s3-error.ts b/ts/classes/s3-error.ts new file mode 100644 index 0000000..c10b470 --- /dev/null +++ b/ts/classes/s3-error.ts @@ -0,0 +1,145 @@ +import * as plugins from '../plugins.js'; + +/** + * S3 error codes mapped to HTTP status codes + */ +const S3_ERROR_CODES: Record = { + 'AccessDenied': 403, + 'BadDigest': 400, + 'BadRequest': 400, + 'BucketAlreadyExists': 409, + 'BucketAlreadyOwnedByYou': 409, + 'BucketNotEmpty': 409, + 'CredentialsNotSupported': 400, + 'EntityTooSmall': 400, + 'EntityTooLarge': 400, + 'ExpiredToken': 400, + 'IncompleteBody': 400, + 'IncorrectNumberOfFilesInPostRequest': 400, + 'InlineDataTooLarge': 400, + 'InternalError': 500, + 'InvalidArgument': 400, + 'InvalidBucketName': 400, + 'InvalidDigest': 400, + 'InvalidLocationConstraint': 400, + 'InvalidPart': 400, + 'InvalidPartOrder': 400, + 'InvalidRange': 416, + 'InvalidRequest': 400, + 'InvalidSecurity': 403, + 'InvalidSOAPRequest': 400, + 'InvalidStorageClass': 400, + 'InvalidTargetBucketForLogging': 400, + 'InvalidToken': 400, + 'InvalidURI': 400, + 'KeyTooLongError': 400, + 'MalformedACLError': 400, + 'MalformedPOSTRequest': 400, + 'MalformedXML': 400, + 'MaxMessageLengthExceeded': 400, + 'MaxPostPreDataLengthExceededError': 400, + 'MetadataTooLarge': 400, + 'MethodNotAllowed': 405, + 'MissingContentLength': 411, + 'MissingRequestBodyError': 400, + 'MissingSecurityElement': 400, + 'MissingSecurityHeader': 400, + 'NoLoggingStatusForKey': 400, + 'NoSuchBucket': 404, + 'NoSuchKey': 404, + 'NoSuchLifecycleConfiguration': 404, + 'NoSuchUpload': 404, + 'NoSuchVersion': 404, + 'NotImplemented': 501, + 'NotSignedUp': 403, + 'OperationAborted': 409, + 'PermanentRedirect': 301, + 'PreconditionFailed': 412, + 'Redirect': 307, + 'RequestIsNotMultiPartContent': 400, + 'RequestTimeout': 400, + 'RequestTimeTooSkewed': 403, + 'RequestTorrentOfBucketError': 400, + 'SignatureDoesNotMatch': 403, + 'ServiceUnavailable': 503, + 'SlowDown': 503, + 'TemporaryRedirect': 307, + 'TokenRefreshRequired': 400, + 'TooManyBuckets': 400, + 'UnexpectedContent': 400, + 'UnresolvableGrantByEmailAddress': 400, + 'UserKeyMustBeSpecified': 400, +}; + +/** + * S3-compatible error class that formats errors as XML responses + */ +export class S3Error extends Error { + public status: number; + public code: string; + public detail: Record; + + constructor( + code: string, + message: string, + detail: Record = {} + ) { + super(message); + this.name = 'S3Error'; + this.code = code; + this.status = S3_ERROR_CODES[code] || 500; + this.detail = detail; + + // Maintain proper stack trace + if (Error.captureStackTrace) { + Error.captureStackTrace(this, S3Error); + } + } + + /** + * Convert error to S3-compatible XML format + */ + public toXML(): string { + const smartXmlInstance = new plugins.SmartXml(); + const errorObj: any = { + Error: { + Code: this.code, + Message: this.message, + ...this.detail, + }, + }; + + const xml = smartXmlInstance.createXmlFromObject(errorObj); + + // Ensure XML declaration + if (!xml.startsWith('\n${xml}`; + } + + return xml; + } + + /** + * Create S3Error from a generic Error + */ + public static fromError(err: any): S3Error { + if (err instanceof S3Error) { + return err; + } + + // Map common errors + if (err.code === 'ENOENT') { + return new S3Error('NoSuchKey', 'The specified key does not exist.'); + } + if (err.code === 'EACCES') { + return new S3Error('AccessDenied', 'Access Denied'); + } + + // Default to internal error + return new S3Error( + 'InternalError', + 'We encountered an internal error. Please try again.', + { OriginalError: err.message } + ); + } +} diff --git a/ts/classes/smarts3-server.ts b/ts/classes/smarts3-server.ts new file mode 100644 index 0000000..ef4c2fb --- /dev/null +++ b/ts/classes/smarts3-server.ts @@ -0,0 +1,239 @@ +import * as plugins from '../plugins.js'; +import { S3Router } from './router.js'; +import { MiddlewareStack } from './middleware-stack.js'; +import { S3Context } from './context.js'; +import { FilesystemStore } from './filesystem-store.js'; +import { S3Error } from './s3-error.js'; +import { ServiceController } from '../controllers/service.controller.js'; +import { BucketController } from '../controllers/bucket.controller.js'; +import { ObjectController } from '../controllers/object.controller.js'; + +export interface ISmarts3ServerOptions { + port?: number; + address?: string; + directory?: string; + cleanSlate?: boolean; + silent?: boolean; +} + +/** + * Custom S3-compatible server implementation + * Built on native Node.js http module with zero framework dependencies + */ +export class Smarts3Server { + private httpServer?: plugins.http.Server; + private router: S3Router; + private middlewares: MiddlewareStack; + private store: FilesystemStore; + private options: Required; + + constructor(options: ISmarts3ServerOptions = {}) { + this.options = { + port: 3000, + address: '0.0.0.0', + directory: plugins.path.join(process.cwd(), '.nogit/bucketsDir'), + cleanSlate: false, + silent: false, + ...options, + }; + + this.store = new FilesystemStore(this.options.directory); + this.router = new S3Router(); + this.middlewares = new MiddlewareStack(); + + this.setupMiddlewares(); + this.setupRoutes(); + } + + /** + * Setup middleware stack + */ + private setupMiddlewares(): void { + // Logger middleware + if (!this.options.silent) { + this.middlewares.use(async (req, res, ctx, next) => { + const start = Date.now(); + console.log(`โ†’ ${req.method} ${req.url}`); + console.log(` Headers:`, JSON.stringify(req.headers, null, 2).slice(0, 200)); + await next(); + const duration = Date.now() - start; + console.log(`โ† ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`); + }); + } + + // TODO: Add authentication middleware + // TODO: Add CORS middleware + } + + /** + * Setup routes + */ + private setupRoutes(): void { + // Service level (/) + this.router.get('/', ServiceController.listBuckets); + + // Bucket level (/:bucket) + this.router.put('/:bucket', BucketController.createBucket); + this.router.delete('/:bucket', BucketController.deleteBucket); + this.router.get('/:bucket', BucketController.listObjects); + this.router.head('/:bucket', BucketController.headBucket); + + // Object level (/:bucket/:key*) + this.router.put('/:bucket/:key*', ObjectController.putObject); + this.router.get('/:bucket/:key*', ObjectController.getObject); + this.router.head('/:bucket/:key*', ObjectController.headObject); + this.router.delete('/:bucket/:key*', ObjectController.deleteObject); + } + + /** + * Handle incoming HTTP request + */ + private async handleRequest( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse + ): Promise { + const context = new S3Context(req, res, this.store); + + try { + // Execute middleware stack + await this.middlewares.execute(req, res, context); + + // Route to handler + const match = this.router.match(context.method, context.url.pathname); + + if (match) { + context.params = match.params; + await match.handler(req, res, context, match.params); + } else { + context.throw('NoSuchKey', 'The specified resource does not exist'); + } + } catch (err) { + await this.handleError(err, context, res); + } + } + + /** + * Handle errors and send S3-compatible error responses + */ + private async handleError( + err: any, + context: S3Context, + res: plugins.http.ServerResponse + ): Promise { + const s3Error = err instanceof S3Error ? err : S3Error.fromError(err); + + if (!this.options.silent) { + console.error(`[S3Error] ${s3Error.code}: ${s3Error.message}`); + if (s3Error.status >= 500) { + console.error(err.stack || err); + } + } + + // Send error response + const errorXml = s3Error.toXML(); + + res.writeHead(s3Error.status, { + 'Content-Type': 'application/xml', + 'Content-Length': Buffer.byteLength(errorXml), + }); + + res.end(errorXml); + } + + /** + * Start the server + */ + public async start(): Promise { + // Initialize store + await this.store.initialize(); + + // Clean slate if requested + if (this.options.cleanSlate) { + await this.store.reset(); + } + + // Create HTTP server + this.httpServer = plugins.http.createServer((req, res) => { + this.handleRequest(req, res).catch((err) => { + console.error('Fatal error in request handler:', err); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } + }); + }); + + // Start listening + await new Promise((resolve, reject) => { + this.httpServer!.listen(this.options.port, this.options.address, (err?: Error) => { + if (err) { + reject(err); + } else { + if (!this.options.silent) { + console.log(`S3 server listening on ${this.options.address}:${this.options.port}`); + } + resolve(); + } + }); + }); + } + + /** + * Stop the server + */ + public async stop(): Promise { + if (!this.httpServer) { + return; + } + + await new Promise((resolve, reject) => { + this.httpServer!.close((err?: Error) => { + if (err) { + reject(err); + } else { + if (!this.options.silent) { + console.log('S3 server stopped'); + } + resolve(); + } + }); + }); + + this.httpServer = undefined; + } + + /** + * Get server port (useful for testing with random ports) + */ + public getPort(): number { + if (!this.httpServer) { + throw new Error('Server not started'); + } + + const address = this.httpServer.address(); + if (typeof address === 'string') { + throw new Error('Unix socket not supported'); + } + + return address?.port || this.options.port; + } + + /** + * Get S3 descriptor for client configuration + */ + public getS3Descriptor(): { + accessKey: string; + accessSecret: string; + endpoint: string; + port: number; + useSsl: boolean; + } { + return { + accessKey: 'S3RVER', + accessSecret: 'S3RVER', + endpoint: this.options.address === '0.0.0.0' ? '127.0.0.1' : this.options.address, + port: this.getPort(), + useSsl: false, + }; + } +} diff --git a/ts/controllers/bucket.controller.ts b/ts/controllers/bucket.controller.ts new file mode 100644 index 0000000..eba6a66 --- /dev/null +++ b/ts/controllers/bucket.controller.ts @@ -0,0 +1,130 @@ +import * as plugins from '../plugins.js'; +import type { S3Context } from '../classes/context.js'; + +/** + * Bucket-level operations + */ +export class BucketController { + /** + * HEAD /:bucket - Check if bucket exists + */ + public static async headBucket( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket } = params; + + if (await ctx.store.bucketExists(bucket)) { + ctx.status(200).send(''); + } else { + ctx.throw('NoSuchBucket', 'The specified bucket does not exist'); + } + } + + /** + * PUT /:bucket - Create bucket + */ + public static async createBucket( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket } = params; + + await ctx.store.createBucket(bucket); + ctx.status(200).send(''); + } + + /** + * DELETE /:bucket - Delete bucket + */ + public static async deleteBucket( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket } = params; + + await ctx.store.deleteBucket(bucket); + ctx.status(204).send(''); + } + + /** + * GET /:bucket - List objects + * Supports both V1 and V2 listing (V2 uses list-type=2 query param) + */ + public static async listObjects( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket } = params; + const isV2 = ctx.query['list-type'] === '2'; + + const result = await ctx.store.listObjects(bucket, { + prefix: ctx.query.prefix, + delimiter: ctx.query.delimiter, + maxKeys: ctx.query['max-keys'] ? parseInt(ctx.query['max-keys']) : 1000, + continuationToken: ctx.query['continuation-token'], + }); + + if (isV2) { + // List Objects V2 response + await ctx.sendXML({ + ListBucketResult: { + '@_xmlns': 'http://s3.amazonaws.com/doc/2006-03-01/', + Name: bucket, + Prefix: result.prefix || '', + MaxKeys: result.maxKeys, + KeyCount: result.contents.length, + IsTruncated: result.isTruncated, + ...(result.delimiter && { Delimiter: result.delimiter }), + ...(result.nextContinuationToken && { + NextContinuationToken: result.nextContinuationToken, + }), + ...(result.commonPrefixes.length > 0 && { + CommonPrefixes: result.commonPrefixes.map((prefix) => ({ + Prefix: prefix, + })), + }), + Contents: result.contents.map((obj) => ({ + Key: obj.key, + LastModified: obj.lastModified.toISOString(), + ETag: `"${obj.md5}"`, + Size: obj.size, + StorageClass: 'STANDARD', + })), + }, + }); + } else { + // List Objects V1 response + await ctx.sendXML({ + ListBucketResult: { + '@_xmlns': 'http://s3.amazonaws.com/doc/2006-03-01/', + Name: bucket, + Prefix: result.prefix || '', + MaxKeys: result.maxKeys, + IsTruncated: result.isTruncated, + ...(result.delimiter && { Delimiter: result.delimiter }), + ...(result.commonPrefixes.length > 0 && { + CommonPrefixes: result.commonPrefixes.map((prefix) => ({ + Prefix: prefix, + })), + }), + Contents: result.contents.map((obj) => ({ + Key: obj.key, + LastModified: obj.lastModified.toISOString(), + ETag: `"${obj.md5}"`, + Size: obj.size, + StorageClass: 'STANDARD', + })), + }, + }); + } + } +} diff --git a/ts/controllers/object.controller.ts b/ts/controllers/object.controller.ts new file mode 100644 index 0000000..f9c2f8d --- /dev/null +++ b/ts/controllers/object.controller.ts @@ -0,0 +1,204 @@ +import * as plugins from '../plugins.js'; +import type { S3Context } from '../classes/context.js'; + +/** + * Object-level operations + */ +export class ObjectController { + /** + * PUT /:bucket/:key* - Upload object or copy object + */ + public static async putObject( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket, key } = params; + + // Check if this is a COPY operation + const copySource = ctx.headers['x-amz-copy-source'] as string | undefined; + if (copySource) { + return ObjectController.copyObject(req, res, ctx, params); + } + + // Extract metadata from headers + const metadata: Record = {}; + for (const [header, value] of Object.entries(ctx.headers)) { + if (header.startsWith('x-amz-meta-')) { + metadata[header] = value as string; + } + if (header === 'content-type' && value) { + metadata['content-type'] = value as string; + } + if (header === 'cache-control' && value) { + metadata['cache-control'] = value as string; + } + } + + // If no content-type, default to binary/octet-stream + if (!metadata['content-type']) { + metadata['content-type'] = 'binary/octet-stream'; + } + + // Stream upload + const result = await ctx.store.putObject(bucket, key, ctx.getRequestStream(), metadata); + + ctx.setHeader('ETag', `"${result.md5}"`); + ctx.status(200).send(''); + } + + /** + * GET /:bucket/:key* - Download object + */ + public static async getObject( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket, key } = params; + + // Parse Range header if present + const rangeHeader = ctx.headers.range as string | undefined; + let range: { start: number; end: number } | undefined; + + if (rangeHeader) { + const matches = rangeHeader.match(/bytes=(\d+)-(\d*)/); + if (matches) { + const start = parseInt(matches[1]); + const end = matches[2] ? parseInt(matches[2]) : undefined; + range = { start, end: end || start + 1024 * 1024 }; // Default to 1MB if no end + } + } + + // Get object + const object = await ctx.store.getObject(bucket, key, range); + + // Set response headers + ctx.setHeader('ETag', `"${object.md5}"`); + ctx.setHeader('Last-Modified', object.lastModified.toUTCString()); + ctx.setHeader('Content-Type', object.metadata['content-type'] || 'binary/octet-stream'); + ctx.setHeader('Accept-Ranges', 'bytes'); + + // Handle custom metadata headers + for (const [key, value] of Object.entries(object.metadata)) { + if (key.startsWith('x-amz-meta-')) { + ctx.setHeader(key, value); + } + } + + if (range) { + ctx.status(206); + ctx.setHeader('Content-Length', (range.end - range.start + 1).toString()); + ctx.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${object.size}`); + } else { + ctx.status(200); + ctx.setHeader('Content-Length', object.size.toString()); + } + + // Stream response + await ctx.send(object.content!); + } + + /** + * HEAD /:bucket/:key* - Get object metadata + */ + public static async headObject( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket, key } = params; + + // Get object (without content) + const object = await ctx.store.getObject(bucket, key); + + // Set response headers (same as GET but no body) + ctx.setHeader('ETag', `"${object.md5}"`); + ctx.setHeader('Last-Modified', object.lastModified.toUTCString()); + ctx.setHeader('Content-Type', object.metadata['content-type'] || 'binary/octet-stream'); + ctx.setHeader('Content-Length', object.size.toString()); + ctx.setHeader('Accept-Ranges', 'bytes'); + + // Handle custom metadata headers + for (const [key, value] of Object.entries(object.metadata)) { + if (key.startsWith('x-amz-meta-')) { + ctx.setHeader(key, value); + } + } + + ctx.status(200).send(''); + } + + /** + * DELETE /:bucket/:key* - Delete object + */ + public static async deleteObject( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket, key } = params; + + await ctx.store.deleteObject(bucket, key); + ctx.status(204).send(''); + } + + /** + * COPY operation (PUT with x-amz-copy-source header) + */ + private static async copyObject( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const { bucket: destBucket, key: destKey } = params; + const copySource = ctx.headers['x-amz-copy-source'] as string; + + // Parse source bucket and key from copy source + // Format: /bucket/key or bucket/key + const sourcePath = copySource.startsWith('/') ? copySource.slice(1) : copySource; + const firstSlash = sourcePath.indexOf('/'); + const srcBucket = decodeURIComponent(sourcePath.slice(0, firstSlash)); + const srcKey = decodeURIComponent(sourcePath.slice(firstSlash + 1)); + + // Get metadata directive (COPY or REPLACE) + const metadataDirective = (ctx.headers['x-amz-metadata-directive'] as string)?.toUpperCase() || 'COPY'; + + // Extract new metadata if REPLACE + let newMetadata: Record | undefined; + if (metadataDirective === 'REPLACE') { + newMetadata = {}; + for (const [header, value] of Object.entries(ctx.headers)) { + if (header.startsWith('x-amz-meta-')) { + newMetadata[header] = value as string; + } + if (header === 'content-type' && value) { + newMetadata['content-type'] = value as string; + } + } + } + + // Perform copy + const result = await ctx.store.copyObject( + srcBucket, + srcKey, + destBucket, + destKey, + metadataDirective as 'COPY' | 'REPLACE', + newMetadata + ); + + // Send XML response + await ctx.sendXML({ + CopyObjectResult: { + LastModified: new Date().toISOString(), + ETag: `"${result.md5}"`, + }, + }); + } +} diff --git a/ts/controllers/service.controller.ts b/ts/controllers/service.controller.ts new file mode 100644 index 0000000..d38c77e --- /dev/null +++ b/ts/controllers/service.controller.ts @@ -0,0 +1,35 @@ +import * as plugins from '../plugins.js'; +import type { S3Context } from '../classes/context.js'; + +/** + * Service-level operations (root /) + */ +export class ServiceController { + /** + * GET / - List all buckets + */ + public static async listBuckets( + req: plugins.http.IncomingMessage, + res: plugins.http.ServerResponse, + ctx: S3Context, + params: Record + ): Promise { + const buckets = await ctx.store.listBuckets(); + + await ctx.sendXML({ + ListAllMyBucketsResult: { + '@_xmlns': 'http://s3.amazonaws.com/doc/2006-03-01/', + Owner: { + ID: '123456789000', + DisplayName: 'S3rver', + }, + Buckets: { + Bucket: buckets.map((bucket) => ({ + Name: bucket.name, + CreationDate: bucket.creationDate.toISOString(), + })), + }, + }, + }); + } +} diff --git a/ts/index.ts b/ts/index.ts index 1308ea8..4a86942 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,9 +1,11 @@ import * as plugins from './plugins.js'; import * as paths from './paths.js'; +import { Smarts3Server } from './classes/smarts3-server.js'; export interface ISmarts3ContructorOptions { port?: number; cleanSlate?: boolean; + useCustomServer?: boolean; // Feature flag for custom server } export class Smarts3 { @@ -18,41 +20,63 @@ export class Smarts3 { // INSTANCE public options: ISmarts3ContructorOptions; - public s3Instance: plugins.s3rver; + public s3Instance: plugins.s3rver | Smarts3Server; constructor(optionsArg: ISmarts3ContructorOptions) { - this.options = optionsArg; this.options = { - ...this.options, + useCustomServer: true, // Default to custom server ...optionsArg, }; } public async start() { - if (this.options.cleanSlate) { - await plugins.smartfile.fs.ensureEmptyDir(paths.bucketsDir); + if (this.options.useCustomServer) { + // Use new custom server + this.s3Instance = new Smarts3Server({ + port: this.options.port || 3000, + address: '0.0.0.0', + directory: paths.bucketsDir, + cleanSlate: this.options.cleanSlate || false, + silent: false, + }); + await this.s3Instance.start(); + console.log('s3 server is running (custom implementation)'); } else { - await plugins.smartfile.fs.ensureDir(paths.bucketsDir); + // Use legacy s3rver + if (this.options.cleanSlate) { + await plugins.smartfile.fs.ensureEmptyDir(paths.bucketsDir); + } else { + await plugins.smartfile.fs.ensureDir(paths.bucketsDir); + } + this.s3Instance = new plugins.s3rver({ + port: this.options.port || 3000, + address: '0.0.0.0', + silent: false, + directory: paths.bucketsDir, + }); + await (this.s3Instance as plugins.s3rver).run(); + console.log('s3 server is running (legacy s3rver)'); } - this.s3Instance = new plugins.s3rver({ - port: this.options.port || 3000, - address: '0.0.0.0', - silent: false, - directory: paths.bucketsDir, - }); - await this.s3Instance.run(); - console.log('s3 server is running'); } public async getS3Descriptor( optionsArg?: Partial, ): Promise { + if (this.options.useCustomServer && this.s3Instance instanceof Smarts3Server) { + const descriptor = this.s3Instance.getS3Descriptor(); + return { + ...descriptor, + ...(optionsArg ? optionsArg : {}), + }; + } + + // Legacy s3rver descriptor return { ...{ accessKey: 'S3RVER', accessSecret: 'S3RVER', endpoint: '127.0.0.1', - port: this.options.port, + port: this.options.port || 3000, useSsl: false, }, ...(optionsArg ? optionsArg : {}), @@ -68,6 +92,13 @@ export class Smarts3 { } public async stop() { - await this.s3Instance.close(); + if (this.s3Instance instanceof Smarts3Server) { + await this.s3Instance.stop(); + } else { + await (this.s3Instance as plugins.s3rver).close(); + } } } + +// Export the custom server class for direct use +export { Smarts3Server } from './classes/smarts3-server.js'; diff --git a/ts/plugins.ts b/ts/plugins.ts index fbfbb40..b92c91a 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -1,14 +1,19 @@ // node native import * as path from 'path'; +import * as http from 'http'; +import * as crypto from 'crypto'; +import * as url from 'url'; +import * as fs from 'fs'; -export { path }; +export { path, http, crypto, url, fs }; // @push.rocks scope import * as smartbucket from '@push.rocks/smartbucket'; import * as smartfile from '@push.rocks/smartfile'; import * as smartpath from '@push.rocks/smartpath'; +import { SmartXml } from '@push.rocks/smartxml'; -export { smartbucket, smartfile, smartpath }; +export { smartbucket, smartfile, smartpath, SmartXml }; // @tsclass scope import * as tsclass from '@tsclass/tsclass'; diff --git a/ts/utils/xml.utils.ts b/ts/utils/xml.utils.ts new file mode 100644 index 0000000..7f0e6b1 --- /dev/null +++ b/ts/utils/xml.utils.ts @@ -0,0 +1,39 @@ +import * as plugins from '../plugins.js'; + +// Create a singleton instance of SmartXml +const smartXmlInstance = new plugins.SmartXml(); + +/** + * Parse XML string to JavaScript object + */ +export function parseXml(xmlString: string): any { + return smartXmlInstance.parseXmlToObject(xmlString); +} + +/** + * Convert JavaScript object to XML string with XML declaration + */ +export function createXml(obj: any, options: { format?: boolean } = {}): string { + const xml = smartXmlInstance.createXmlFromObject(obj); + + // Ensure XML declaration is present + if (!xml.startsWith('\n${xml}`; + } + + return xml; +} + +/** + * Helper to create S3-compatible XML responses with proper namespace + */ +export function createS3Xml(rootElement: string, content: any, namespace = 'http://s3.amazonaws.com/doc/2006-03-01/'): string { + const obj: any = { + [rootElement]: { + '@_xmlns': namespace, + ...content, + }, + }; + + return createXml(obj, { format: true }); +}