diff --git a/.smartconfig.json b/.smartconfig.json index 5057588..c5b5c2e 100644 --- a/.smartconfig.json +++ b/.smartconfig.json @@ -19,5 +19,19 @@ "dockerregistry.lossless.digital": "serve.zone/siprouter" }, "platforms": ["linux/amd64", "linux/arm64"] + }, + "@git.zone/cli": { + "release": { + "targets": { + "git": { + "enabled": true, + "remote": "origin" + }, + "docker": { + "enabled": true, + "engine": "tsdocker" + } + } + } } } diff --git a/changelog.md b/changelog.md index cb7f8a5..9b1cf6c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## Pending + +### Features + +- persist siprouter config and media through SmartData and SmartBucket (storage) + - store runtime config, voicemail metadata, fax jobs, and fax inbox metadata in SmartData + - store voicemail audio, custom greetings, and fax payloads in SmartBucket while keeping local cache paths for Rust media access + - migrate legacy local voicemail and fax metadata/media into SmartData and SmartBucket on startup + - enable gitzone Docker release publishing through the configured tsdocker target + ## 2026-04-20 - 1.26.0 - feat(fax) add fax routing, job tracking, inbox management, and T.38/UDPTL media support @@ -337,4 +347,4 @@ Initial SIP-aware proxy for Grandstream HT801 ↔ easybell connectivity. - Added SDP rewriting and per-call RTP relay sockets - Added NAT priming and G.722 silence streaming after `200 OK` so easybell detects inbound media promptly - Inserted `Record-Route` so in-dialog ACK/BYE/re-INVITE continue through the proxy -- Included captured device setting snapshots and setup documentation for diagnosing registration issues \ No newline at end of file +- Included captured device setting snapshots and setup documentation for diagnosing registration issues diff --git a/package.json b/package.json index 48f5956..d8e124f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "bundle": "node node_modules/.pnpm/esbuild@0.27.7/node_modules/esbuild/bin/esbuild ts_web/index.ts --bundle --format=esm --outfile=dist_ts_web/bundle.js --platform=browser --target=es2022 --minify", + "bundle": "esbuild ts_web/index.ts --bundle --format=esm --outfile=dist_ts_web/bundle.js --platform=browser --target=es2022 --minify", "buildRust": "tsrust", "build": "pnpm run buildRust && pnpm run bundle", "build:docker": "tsdocker build --verbose", @@ -15,6 +15,8 @@ "dependencies": { "@design.estate/dees-catalog": "^3.81.0", "@design.estate/dees-element": "^2.2.4", + "@push.rocks/smartbucket": "^4.6.1", + "@push.rocks/smartdata": "^7.1.7", "@push.rocks/smartrust": "^1.4.0", "@push.rocks/smartstate": "^2.3.1", "tsx": "^4.21.0", @@ -25,6 +27,17 @@ "@git.zone/tsdocker": "^2.2.5", "@git.zone/tsrust": "^1.3.3", "@git.zone/tswatch": "^3.3.3", - "@types/ws": "^8.18.1" + "@types/node": "^25.8.0", + "@types/ws": "^8.18.1", + "esbuild": "^0.27.7" + }, + "pnpm": { + "ignoredBuiltDependencies": [ + "@design.estate/dees-catalog" + ], + "onlyBuiltDependencies": [ + "esbuild", + "mongodb-memory-server" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dd6f65..83e9587 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@design.estate/dees-element': specifier: ^2.2.4 version: 2.2.4 + '@push.rocks/smartbucket': + specifier: ^4.6.1 + version: 4.6.1 + '@push.rocks/smartdata': + specifier: ^7.1.7 + version: 7.1.7 '@push.rocks/smartrust': specifier: ^1.4.0 version: 1.4.0 @@ -39,9 +45,15 @@ importers: '@git.zone/tswatch': specifier: ^3.3.3 version: 3.3.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@tiptap/pm@2.27.2) + '@types/node': + specifier: ^25.8.0 + version: 25.8.0 '@types/ws': specifier: ^8.18.1 version: 8.18.1 + esbuild: + specifier: ^0.27.7 + version: 0.27.7 packages: @@ -65,6 +77,161 @@ packages: '@apiglobal/typedrequest-interfaces@1.0.20': resolution: {integrity: sha512-ybsDtavYbzGJYSLodSbkxDvSLYtfMzBTuNZDJpiANt1rZA2MO/GCq8zk5MVLlrUUQIr/7oxPGWqxi1QDwR+RHQ==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1047.0': + resolution: {integrity: sha512-gk8g31eqvgf7eLCpkVjWs9KL7gYgkomt3FT2o9tbIe6goYrBheN2lHxhCsTn1zFYbt7EwrZXTGkQPIQNIN0c5w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.10': + resolution: {integrity: sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.8': + resolution: {integrity: sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.36': + resolution: {integrity: sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.38': + resolution: {integrity: sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.40': + resolution: {integrity: sha512-0NFGS9I3PD2yMveQqqpwpRdyZVStzgk0Yr2rZHh80kV/QNqQCK5lSrksvU3nBcRNSUF5Uk8rL3Xk0EVR+UVAnA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.40': + resolution: {integrity: sha512-IEIl+UQnrEjZP53TSl91e8LBephi4i1Mt9WZrMgN8pOg6xPOLZdkN1GhsEzjkMD1TQy4Fp2dwWA/9ToTQFOlLA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.41': + resolution: {integrity: sha512-h6BlclpsPGkx7Pv7ukr8oKVqN3jvxnH5n9ZIUQa8focr1ZkKd2MYiPJ2Nv9GI97dohJVJBfZAsTp/qoZL5R1pw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.36': + resolution: {integrity: sha512-eDQ6X7clTAOxXegOx4rGT1hyfusGEYdJGCGo0Ym2+CKeMQBjk+SJSxSVev11NJew5xJHJ/c3hryl2awKaxuSEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.40': + resolution: {integrity: sha512-jaABbsoOkGlKg5kaHetYmUV6mWM57H89ia0Yksom1XxC847mfjmEVb4p7VijS1sjPbXjUii4cftJuwsl4MXkRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.40': + resolution: {integrity: sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.12': + resolution: {integrity: sha512-MAG0Adg7FFEwuoeLbb5SBnXDW7S2EpNTwHnQ4h3pJqSKVQOhOmugyA1MfMh6AD4SAfx0lko4htZdwkNoLqFj5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.11': + resolution: {integrity: sha512-xpobcctR1AHSrvkiArgTyLffn78Lt9unPMpa/yic9RKn+bOf/5M55UIM6RaPL5xKzI06/GSsTDywTWvzEAbyyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.18': + resolution: {integrity: sha512-2noO+4ARfC+8vOIyvJvQE6bioVaTRkUcPvUoM/jgwXcweZnZovSZ6OCs/cs+NU2p7yvuwuJT/7LkTzBSj5pU4A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.11': + resolution: {integrity: sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.10': + resolution: {integrity: sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.12': + resolution: {integrity: sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.39': + resolution: {integrity: sha512-cimoQxecHHNad+lv2g7QJ24Cxqh1P0EULJSxyX4YD95BUIGeGRPumbdEXpHPxNkJRU99DVmh7u16Y+uhFu31Yw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.10': + resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.40': + resolution: {integrity: sha512-QLpD+HNQtL1Mc49/GRa6RmZvi/TEYBWPevC9F3L+j96IoG3xOSRctdQfbkX0lETb3TX9QQXU1oGYDmAB+YJprA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.8': + resolution: {integrity: sha512-/Vw2M27w+0APfMDzDpvv8auA4WiJ4D22+lC61pMS2M8Wk+4IydeRqh5utbrh+A5gQRxgUYd/xz3tdv8nQlmiHg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.14': + resolution: {integrity: sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.26': + resolution: {integrity: sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1047.0': + resolution: {integrity: sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.9': + resolution: {integrity: sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.11': + resolution: {integrity: sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==} + + '@aws-sdk/util-user-agent-node@3.973.26': + resolution: {integrity: sha512-9bHR/EERjhrUGyo1qW620ogbGBtCglYB/pEtcm85sVd4/Ah+bwdLI3g1aJf75oNwNwh7+fw+8wOk/OCWHjzVmA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.24': + resolution: {integrity: sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} @@ -388,6 +555,9 @@ packages: '@module-federation/webpack-bundler-runtime@0.22.0': resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + '@mongodb-js/saslprep@1.4.11': + resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==} + '@napi-rs/canvas-android-arm64@0.1.100': resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} engines: {node: '>= 10'} @@ -493,12 +663,18 @@ packages: '@push.rocks/lik@6.4.1': resolution: {integrity: sha512-W5M2zoJWUxYnCVqUB7jaxMB4W1kfhs1P6SXvWGqwDpJAjMjCnZeAXD+w0akECgSBY1zCCT2qMj7YK4Gza0t25g==} + '@push.rocks/mongodump@1.1.1': + resolution: {integrity: sha512-sXbTlS5UxL0XDQFwrGfDrpB4FyxQm41WOlM/AZw0rUoWIQgrAa/0O02ijjHBPX0vZw3RHPD6NDYbKBGxULXiCg==} + '@push.rocks/projectinfo@5.1.0': resolution: {integrity: sha512-jd+aP/UpCVA+kxK7qr1PqMUb5oRIGxukUIi6Qtlp6KKX0jBoaTFvgtEH+cnd3ilL4oNdYNsXMNwvfv4KOmMeVw==} '@push.rocks/qenv@6.1.4': resolution: {integrity: sha512-NlDwrb3KJVBCeEXIWaYRZXZLOvHhDoo+n2X5akcGCDjn5HyP0C9/opn2RDpCnSt+hoValKpp89wcX4BEB+gWjA==} + '@push.rocks/smartbucket@4.6.1': + resolution: {integrity: sha512-dh2xfAKOf9MKcJkkWImGCpoUmh10gDYQbeyEMO478H0ozxsf9mxgImRMX4a57BYzXGJ1LtoImdTqX2sFoltB3g==} + '@push.rocks/smartbuffer@3.0.6': resolution: {integrity: sha512-1jXfAOsisgDZS+L1E5OCLcM1dseO2rpuqGtSbeB89IDMiBeBTYbzcQ7ZkQQpFIzEzpzjvtl6COT6ZVxCtGnhGA==} @@ -511,6 +687,9 @@ packages: '@push.rocks/smartconfig@6.1.1': resolution: {integrity: sha512-coEpt1s0QII5cUh+Vj9E57iuuOlsn3ecTJOuo/ry9npSYDE9oapKgDO/odXBRNQYBsIlF8jEtXr+LdO0mZNTMw==} + '@push.rocks/smartdata@7.1.7': + resolution: {integrity: sha512-HDI/Q9dKybfsJ68oCzlE+S63Xpij9qXnMfi28yznKP0Li1ECVZZMDDGIW5IjsXlHjO+Q+RJMcVd72Pjt3QLY5Q==} + '@push.rocks/smartdelay@3.1.0': resolution: {integrity: sha512-59xveBMbWmbFhh/rqhQnYG/klg/VONG9hV8+RQ7ftqsNRkcmUT+VM5etAbODgAUvsF4lxK+xVR0tbZOo0kGhRQ==} @@ -580,6 +759,9 @@ packages: '@push.rocks/smartmime@2.0.4': resolution: {integrity: sha512-mG6lRBLr5nF+GLZmgCcdjhdDsmTtJWBFZDCa1eJ8Au9TvUzbPW0fY5aqJBb3UwfyZzH6St8Th9cJSXjagOQkYA==} + '@push.rocks/smartmongo@5.1.1': + resolution: {integrity: sha512-OFzEjTlXQ0zN9KYewhJRJxxX8bdVO7sl5H4RRd0F0PyU4FEXesLF8Sm4rsCFtQW1ifGQEBOcoruRkoiWz918Ug==} + '@push.rocks/smartntml@2.0.9': resolution: {integrity: sha512-6g8kf6Ag2864A+S79RBSZjV8xHBq82YC83j5TbG71aPLoGiy+YREg9HiIOLN50j8/hk3PEkM21YDOGhRle6R8Q==} @@ -865,6 +1047,42 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@smithy/core@3.24.2': + resolution: {integrity: sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.2': + resolution: {integrity: sha512-iYr9ekBjmZ+FwkiHEopqGscBbl78X62cq3p5Dd0eC+gNd7fybNZFQQdDuOQjTVmFymleuA8YRWZnuXWZ8B3kKA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.2': + resolution: {integrity: sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.2': + resolution: {integrity: sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.2': + resolution: {integrity: sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@tempfix/lenis@1.3.20': resolution: {integrity: sha512-ypeB0FuHLHOCQXW4d0RQ69txPJJH+1CHcpsZIUdcv2t1vR0IVyQr2vHihtde9UOXhjzqEnUphWon/UcJNsa0YA==} peerDependencies: @@ -1077,6 +1295,9 @@ packages: '@types/node@25.6.1': resolution: {integrity: sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g==} + '@types/node@25.8.0': + resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} + '@types/randomatic@3.1.5': resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==} @@ -1104,9 +1325,15 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/whatwg-url@13.0.0': + resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/which@3.0.4': resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==} @@ -1119,6 +1346,10 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -1146,9 +1377,20 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + asynckit@0.4.0: resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1159,9 +1401,53 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.8.3: + resolution: {integrity: sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.1: + resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.1: + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.3: + resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} @@ -1172,6 +1458,13 @@ packages: broadcast-channel@7.3.0: resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==} + bson@7.2.0: + resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} + engines: {node: '>=20.19.0'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} + buffer-json@2.0.0: resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} @@ -1197,6 +1490,10 @@ packages: camel-case@3.0.0: resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1267,6 +1564,9 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commondir@1.0.1: + resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -1387,6 +1687,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1404,6 +1707,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1425,6 +1731,23 @@ packages: resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} engines: {node: '>=18'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -1520,6 +1843,10 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + humanize-ms@1.2.1: resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} @@ -1620,6 +1947,10 @@ packages: lit@3.3.2: resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + lodash.clonedeep@4.5.0: resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} @@ -1712,6 +2043,9 @@ packages: mediabunny@1.44.1: resolution: {integrity: sha512-Frx7nlYFDHPYPgkn3JsWsAExetSIImr5F1Hoz0+fXvV9OAXs02LVtrlmtj/SVaF1RPEUPyeIo9wRBH685u79pQ==} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -1816,6 +2150,9 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mingo@7.2.1: + resolution: {integrity: sha512-MEIQPOSJS2sVCueyQeE2rzgEeW3HpIIhizPbeuwD4v7+miVj7NI3ZVPqqw8t3YPIWCivpIaXA4KsoRI7koyNOA==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1831,6 +2168,45 @@ packages: monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + mongodb-connection-string-url@7.0.1: + resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} + engines: {node: '>=20.19.0'} + + mongodb-memory-server-core@11.1.0: + resolution: {integrity: sha512-GwpnJVIiUyXdi5BoTsExrvLupSt3sJzCSX5P6fxlr0dCrJkhumiq8SQIqtTBqTu2mMpFMCHdjSS0QMUvFMpbWw==} + engines: {node: '>=20.19.0'} + + mongodb-memory-server@11.1.0: + resolution: {integrity: sha512-x9psV1KXRgG5t14AmsrfcWCqlNXvPOzcyroMSeRU5vkAm8jxEF5WiLGdGCONLOgeCNjRnpg6igyDum/eTwiooA==} + engines: {node: '>=20.19.0'} + + mongodb@7.2.0: + resolution: {integrity: sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.806.0 + '@mongodb-js/zstd': ^7.0.0 + gcp-metadata: ^7.0.1 + kerberos: ^7.0.0 + mongodb-client-encryption: '>=7.0.0 <7.1.0' + snappy: ^7.3.2 + socks: ^2.8.6 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1846,6 +2222,10 @@ packages: engines: {node: ^14 || ^16 || >=18} hasBin: true + new-find-package-json@2.0.0: + resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==} + engines: {node: '>=12.22.0'} + no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} @@ -1891,6 +2271,14 @@ packages: resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=} engines: {node: '>=4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -1899,6 +2287,10 @@ packages: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1909,6 +2301,10 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-expression-matcher@1.5.0: resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} @@ -1932,10 +2328,17 @@ packages: resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} engines: {node: '>=14.16'} + pend@1.2.0: + resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -2008,6 +2411,10 @@ packages: punycode@1.4.1: resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.15.1: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} @@ -2084,6 +2491,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2130,10 +2542,16 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sparse-bitfield@3.0.3: + resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=} + spawn-wrap@3.0.0: resolution: {integrity: sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==} engines: {node: '>=8'} + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2180,6 +2598,15 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + threads@1.7.0: resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} @@ -2197,6 +2624,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2260,6 +2691,9 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -2307,10 +2741,18 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2354,6 +2796,10 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl@3.3.0: + resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} + engines: {node: '>=12'} + yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} @@ -2443,6 +2889,360 @@ snapshots: '@apiglobal/typedrequest-interfaces@1.0.20': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1047.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/credential-provider-node': 3.972.41 + '@aws-sdk/middleware-bucket-endpoint': 3.972.12 + '@aws-sdk/middleware-expect-continue': 3.972.11 + '@aws-sdk/middleware-flexible-checksums': 3.974.18 + '@aws-sdk/middleware-host-header': 3.972.11 + '@aws-sdk/middleware-location-constraint': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-sdk-s3': 3.972.39 + '@aws-sdk/middleware-ssec': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.26 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.974.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.24 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/credential-provider-env': 3.972.36 + '@aws-sdk/credential-provider-http': 3.972.38 + '@aws-sdk/credential-provider-login': 3.972.40 + '@aws-sdk/credential-provider-process': 3.972.36 + '@aws-sdk/credential-provider-sso': 3.972.40 + '@aws-sdk/credential-provider-web-identity': 3.972.40 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/credential-provider-imds': 4.3.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.41': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.36 + '@aws-sdk/credential-provider-http': 3.972.38 + '@aws-sdk/credential-provider-ini': 3.972.40 + '@aws-sdk/credential-provider-process': 3.972.36 + '@aws-sdk/credential-provider-sso': 3.972.40 + '@aws-sdk/credential-provider-web-identity': 3.972.40 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/credential-provider-imds': 4.3.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/token-providers': 3.1047.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.12': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.18': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/crc64-nvme': 3.972.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.12': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.39': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.8': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/middleware-host-header': 3.972.11 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.26 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.14': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.26': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1047.0': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.9': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.26': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.24': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/runtime@7.28.6': {} '@borewit/text-codec@0.2.2': {} @@ -2894,6 +3694,10 @@ snapshots: '@module-federation/runtime': 0.22.0 '@module-federation/sdk': 0.22.0 + '@mongodb-js/saslprep@1.4.11': + dependencies: + sparse-bitfield: 3.0.3 + '@napi-rs/canvas-android-arm64@0.1.100': optional: true @@ -2986,6 +3790,23 @@ snapshots: '@types/symbol-tree': 3.2.5 symbol-tree: 3.2.4 + '@push.rocks/mongodump@1.1.1': + dependencies: + '@push.rocks/lik': 6.4.1 + '@push.rocks/smartfs': 1.5.1 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartpromise': 4.2.4 + '@tsclass/tsclass': 9.5.1 + mongodb: 7.2.0 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + '@push.rocks/projectinfo@5.1.0': dependencies: '@push.rocks/smartfs': 1.5.1 @@ -3001,6 +3822,21 @@ snapshots: '@push.rocks/smartpath': 6.0.0 yaml: 2.8.4 + '@push.rocks/smartbucket@4.6.1': + dependencies: + '@aws-sdk/client-s3': 3.1047.0 + '@push.rocks/smartmime': 2.0.4 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartpromise': 4.2.4 + '@push.rocks/smartrx': 3.0.10 + '@push.rocks/smartstream': 3.4.2 + '@push.rocks/smartstring': 4.1.1 + '@push.rocks/smartunique': 3.0.9 + '@tsclass/tsclass': 9.5.1 + minimatch: 10.2.5 + transitivePeerDependencies: + - aws-crt + '@push.rocks/smartbuffer@3.0.6': dependencies: uint8array-extras: 1.5.0 @@ -3040,6 +3876,36 @@ snapshots: - supports-color - vue + '@push.rocks/smartdata@7.1.7': + dependencies: + '@push.rocks/lik': 6.4.1 + '@push.rocks/smartdelay': 3.1.0 + '@push.rocks/smartlog': 3.2.2 + '@push.rocks/smartmongo': 5.1.1 + '@push.rocks/smartpromise': 4.2.4 + '@push.rocks/smartrx': 3.0.10 + '@push.rocks/smartstring': 4.1.1 + '@push.rocks/smarttime': 4.2.3 + '@push.rocks/smartunique': 3.0.9 + '@push.rocks/taskbuffer': 8.0.2 + '@tsclass/tsclass': 9.5.1 + mongodb: 7.2.0 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - '@nuxt/kit' + - bare-abort-controller + - bare-buffer + - gcp-metadata + - kerberos + - mongodb-client-encryption + - react + - react-native-b4a + - snappy + - socks + - supports-color + - vue + '@push.rocks/smartdelay@3.1.0': dependencies: '@push.rocks/smartpromise': 4.2.4 @@ -3180,6 +4046,33 @@ snapshots: file-type: 19.6.0 mime: 4.1.0 + '@push.rocks/smartmongo@5.1.1': + dependencies: + '@push.rocks/mongodump': 1.1.1 + '@push.rocks/smartdata': 7.1.7 + '@push.rocks/smartfs': 1.5.1 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartpromise': 4.2.4 + '@push.rocks/smartrx': 3.0.10 + bson: 7.2.0 + mingo: 7.2.1 + mongodb-memory-server: 11.1.0 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - '@nuxt/kit' + - bare-abort-controller + - bare-buffer + - gcp-metadata + - kerberos + - mongodb-client-encryption + - react + - react-native-b4a + - snappy + - socks + - supports-color + - vue + '@push.rocks/smartntml@2.0.9': dependencies: '@design.estate/dees-element': 2.2.4 @@ -3524,6 +4417,54 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@smithy/core@3.24.2': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@tempfix/lenis@1.3.20': {} '@tempfix/webcontainer__api@1.6.1': {} @@ -3749,6 +4690,10 @@ snapshots: dependencies: undici-types: 7.19.2 + '@types/node@25.8.0': + dependencies: + undici-types: 7.24.6 + '@types/randomatic@3.1.5': {} '@types/relateurl@0.2.33': {} @@ -3771,8 +4716,14 @@ snapshots: '@types/uuid@9.0.8': {} + '@types/webidl-conversions@7.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/whatwg-url@13.0.0': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/which@3.0.4': {} '@types/wrap-ansi@3.0.0': {} @@ -3783,6 +4734,8 @@ snapshots: '@ungap/structured-clone@1.3.1': {} + agent-base@7.1.4: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -3805,16 +4758,56 @@ snapshots: argparse@2.0.1: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + asynckit@0.4.0: {} + b4a@1.8.1: {} + bail@2.0.2: {} balanced-match@1.0.2: {} balanced-match@4.0.4: {} + bare-events@2.8.3: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.3 + bare-path: 3.0.0 + bare-stream: 2.13.1(bare-events@2.8.3) + bare-url: 2.4.3 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.1: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.1 + + bare-stream@2.13.1(bare-events@2.8.3): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.3 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.3: + dependencies: + bare-path: 3.0.0 + base64-js@1.5.1: {} + bowser@2.14.1: {} + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 @@ -3830,6 +4823,10 @@ snapshots: p-queue: 6.6.2 unload: 2.4.1 + bson@7.2.0: {} + + buffer-crc32@0.2.13: {} + buffer-json@2.0.0: {} buffer@6.0.3: @@ -3861,6 +4858,8 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + camelcase@6.3.0: {} + ccount@2.0.1: {} chalk@2.4.2: @@ -3920,6 +4919,8 @@ snapshots: commander@2.20.3: {} + commondir@1.0.1: {} + crelt@1.0.6: {} croner@10.0.1: {} @@ -4047,6 +5048,12 @@ snapshots: eventemitter3@4.0.7: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.3 + transitivePeerDependencies: + - bare-abort-controller + extend@3.0.2: {} external-editor@3.1.0: @@ -4061,6 +5068,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-json-stable-stringify@2.1.0: {} fast-xml-builder@1.1.9: @@ -4089,6 +5098,21 @@ snapshots: token-types: 6.1.2 uint8array-extras: 1.5.0 + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + foreground-child@2.0.0: dependencies: cross-spawn: 7.0.6 @@ -4214,6 +5238,13 @@ snapshots: html-void-elements@3.0.0: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -4304,6 +5335,10 @@ snapshots: lit-element: 4.2.2 lit-html: 3.3.2 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + lodash.clonedeep@4.5.0: {} log-symbols@3.0.0: @@ -4475,6 +5510,8 @@ snapshots: '@types/dom-mediacapture-transform': 0.1.11 '@types/dom-webcodecs': 0.1.13 + memory-pager@1.5.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -4683,6 +5720,8 @@ snapshots: mimic-fn@2.1.0: {} + mingo@7.2.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -4698,6 +5737,61 @@ snapshots: dompurify: 3.2.7 marked: 14.0.0 + mongodb-connection-string-url@7.0.1: + dependencies: + '@types/whatwg-url': 13.0.0 + whatwg-url: 14.2.0 + + mongodb-memory-server-core@11.1.0: + dependencies: + async-mutex: 0.5.0 + camelcase: 6.3.0 + debug: 4.4.3 + find-cache-dir: 3.3.2 + follow-redirects: 1.16.0(debug@4.4.3) + https-proxy-agent: 7.0.6 + mongodb: 7.2.0 + new-find-package-json: 2.0.0 + semver: 7.8.0 + tar-stream: 3.2.0 + tslib: 2.8.1 + yauzl: 3.3.0 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - bare-abort-controller + - bare-buffer + - gcp-metadata + - kerberos + - mongodb-client-encryption + - react-native-b4a + - snappy + - socks + - supports-color + + mongodb-memory-server@11.1.0: + dependencies: + mongodb-memory-server-core: 11.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - bare-abort-controller + - bare-buffer + - gcp-metadata + - kerberos + - mongodb-client-encryption + - react-native-b4a + - snappy + - socks + - supports-color + + mongodb@7.2.0: + dependencies: + '@mongodb-js/saslprep': 1.4.11 + bson: 7.2.0 + mongodb-connection-string-url: 7.0.1 + ms@2.1.3: {} mute-stream@0.0.8: {} @@ -4706,6 +5800,12 @@ snapshots: nanoid@4.0.2: {} + new-find-package-json@2.0.0: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + no-case@2.3.2: dependencies: lower-case: 1.1.4 @@ -4749,6 +5849,14 @@ snapshots: p-finally@1.0.0: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -4758,6 +5866,8 @@ snapshots: dependencies: p-finally: 1.0.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} param-case@2.1.1: @@ -4766,6 +5876,8 @@ snapshots: parse-ms@4.0.0: {} + path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} path-key@3.1.1: {} @@ -4783,8 +5895,14 @@ snapshots: peek-readable@5.4.2: {} + pend@1.2.0: {} + picomatch@4.0.4: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -4898,6 +6016,8 @@ snapshots: punycode@1.4.1: {} + punycode@2.3.1: {} + qs@6.15.1: dependencies: side-channel: 1.1.0 @@ -5013,6 +6133,8 @@ snapshots: semver@6.3.1: {} + semver@7.8.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5066,6 +6188,10 @@ snapshots: space-separated-tokens@2.0.2: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + spawn-wrap@3.0.0: dependencies: cross-spawn: 7.0.6 @@ -5076,6 +6202,15 @@ snapshots: signal-exit: 3.0.7 which: 2.0.2 + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5120,6 +6255,30 @@ snapshots: tagged-tag@1.0.0: {} + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + threads@1.7.0: dependencies: callsites: 3.1.0 @@ -5149,6 +6308,10 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -5192,6 +6355,8 @@ snapshots: undici-types@7.19.2: {} + undici-types@7.24.6: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -5254,8 +6419,15 @@ snapshots: dependencies: defaults: 1.0.4 + webidl-conversions@7.0.0: {} + whatwg-mimetype@3.0.0: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5282,6 +6454,11 @@ snapshots: yargs-parser@22.0.0: {} + yauzl@3.3.0: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + yoctocolors-cjs@2.1.3: {} zrender@5.6.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..0cae360 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + esbuild: true + mongodb-memory-server: true +ignoredBuiltDependencies: + - '@design.estate/dees-catalog' diff --git a/ts/config.ts b/ts/config.ts index e0b3bfc..88f7cd4 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -1,13 +1,10 @@ /** - * Application configuration — loaded from .nogit/config.json. + * Application configuration models and normalization helpers. * * All network addresses, credentials, provider settings, device definitions, - * and routing rules come from this single config file. No hardcoded values - * in source. + * and routing rules are persisted through SmartData. */ -import fs from 'node:fs'; -import path from 'node:path'; import type { IFaxBoxConfig } from './faxbox.ts'; import type { IVoiceboxConfig } from './voicebox.js'; @@ -266,97 +263,197 @@ export interface IAppConfig { } // --------------------------------------------------------------------------- -// Loader +// Defaults and normalization // --------------------------------------------------------------------------- -const CONFIG_PATH = path.join(process.cwd(), '.nogit', 'config.json'); +function requiredInitialEnv(keyArg: string): string { + const value = process.env[keyArg]; + if (!value) { + throw new Error(`Missing required initial config environment variable: ${keyArg}`); + } + return value; +} -export function loadConfig(): IAppConfig { - let raw: string; +function numberFromEnv(keyArg: string, fallbackArg: number): number { + const value = process.env[keyArg]; + if (!value) return fallbackArg; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallbackArg; +} + +export function normalizeConfig(cfg: IAppConfig): IAppConfig { try { - raw = fs.readFileSync(CONFIG_PATH, 'utf8'); - } catch { - throw new Error(`config not found at ${CONFIG_PATH} — create .nogit/config.json`); - } - - const cfg = JSON.parse(raw) as IAppConfig; - - // Basic validation. - if (!cfg.proxy) throw new Error('config: missing "proxy" section'); - if (!cfg.proxy.lanIp) throw new Error('config: missing proxy.lanIp'); - if (!cfg.proxy.lanPort) throw new Error('config: missing proxy.lanPort'); - if (!cfg.proxy.rtpPortRange?.min || !cfg.proxy.rtpPortRange?.max) { - throw new Error('config: missing proxy.rtpPortRange.min/max'); - } - cfg.proxy.webUiPort ??= 3060; - cfg.proxy.publicIpSeed ??= null; - - cfg.providers ??= []; - for (const p of cfg.providers) { - if (!p.id || !p.domain || !p.outboundProxy || !p.username || !p.password) { - throw new Error(`config: provider "${p.id || '?'}" missing required fields`); + // Basic validation. + if (!cfg.proxy) throw new Error('config: missing "proxy" section'); + if (!cfg.proxy.lanIp) throw new Error('config: missing proxy.lanIp'); + if (!cfg.proxy.lanPort) throw new Error('config: missing proxy.lanPort'); + if (!cfg.proxy.rtpPortRange?.min || !cfg.proxy.rtpPortRange?.max) { + throw new Error('config: missing proxy.rtpPortRange.min/max'); } - p.displayName ??= p.id; - p.registerIntervalSec ??= 300; - p.codecs ??= [9, 0, 8, 101]; - p.quirks ??= { earlyMediaSilence: false }; - } + cfg.proxy.webUiPort ??= 3060; + cfg.proxy.publicIpSeed ??= null; - if (!Array.isArray(cfg.devices) || !cfg.devices.length) { - throw new Error('config: need at least one device'); - } - for (const d of cfg.devices) { - if (!d.id || !d.expectedAddress) { - throw new Error(`config: device "${d.id || '?'}" missing required fields`); + cfg.providers ??= []; + for (const p of cfg.providers) { + if (!p.id || !p.domain || !p.outboundProxy || !p.username || !p.password) { + throw new Error(`config: provider "${p.id || '?'}" missing required fields`); + } + p.displayName ??= p.id; + p.registerIntervalSec ??= 300; + p.codecs ??= [9, 0, 8, 101]; + p.quirks ??= { earlyMediaSilence: false }; } - d.displayName ??= d.id; - d.extension ??= '100'; + + if (!Array.isArray(cfg.devices) || !cfg.devices.length) { + throw new Error('config: need at least one device'); + } + for (const d of cfg.devices) { + if (!d.id || !d.expectedAddress) { + throw new Error(`config: device "${d.id || '?'}" missing required fields`); + } + d.displayName ??= d.id; + d.extension ??= '100'; + } + + cfg.incomingNumbers ??= []; + for (const incoming of cfg.incomingNumbers) { + if (!incoming.id) incoming.id = `incoming-${Date.now()}`; + incoming.label ??= incoming.id; + incoming.mode ??= incoming.pattern ? 'regex' : incoming.rangeStart || incoming.rangeEnd ? 'range' : 'single'; + incoming.countryCode ??= incoming.mode === 'regex' ? undefined : '+49'; + } + + cfg.routing ??= { routes: [] }; + cfg.routing.routes ??= []; + + cfg.contacts ??= []; + for (const c of cfg.contacts) { + c.starred ??= false; + } + + cfg.faxboxes ??= []; + for (const fb of cfg.faxboxes) { + fb.enabled ??= true; + fb.maxMessages ??= 50; + } + + cfg.voiceboxes ??= []; + for (const vb of cfg.voiceboxes) { + vb.enabled ??= true; + vb.noAnswerTimeoutSec ??= 25; + vb.maxRecordingSec ??= 120; + vb.maxMessages ??= 50; + vb.greetingVoice ??= 'af_bella'; + } + + if (cfg.ivr) { + cfg.ivr.enabled ??= false; + cfg.ivr.menus ??= []; + for (const menu of cfg.ivr.menus) { + menu.timeoutSec ??= 5; + menu.maxRetries ??= 3; + menu.entries ??= []; + } + } + + return cfg; + } catch (error) { + throw error; } +} - cfg.incomingNumbers ??= []; - for (const incoming of cfg.incomingNumbers) { - if (!incoming.id) incoming.id = `incoming-${Date.now()}`; - incoming.label ??= incoming.id; - incoming.mode ??= incoming.pattern ? 'regex' : incoming.rangeStart || incoming.rangeEnd ? 'range' : 'single'; - incoming.countryCode ??= incoming.mode === 'regex' ? undefined : '+49'; - } +export function createInitialConfigFromEnv(): IAppConfig { + return normalizeConfig({ + proxy: { + lanIp: requiredInitialEnv('SIPROUTER_LAN_IP'), + lanPort: numberFromEnv('SIPROUTER_LAN_PORT', 5070), + publicIpSeed: process.env.SIPROUTER_PUBLIC_IP || null, + rtpPortRange: { + min: numberFromEnv('SIPROUTER_RTP_PORT_MIN', 20000), + max: numberFromEnv('SIPROUTER_RTP_PORT_MAX', 20200), + }, + webUiPort: numberFromEnv('SIPROUTER_WEB_UI_PORT', 3060), + }, + providers: [], + devices: [ + { + id: process.env.SIPROUTER_INITIAL_DEVICE_ID || 'desk-phone', + displayName: process.env.SIPROUTER_INITIAL_DEVICE_DISPLAY_NAME || 'Desk Phone', + expectedAddress: requiredInitialEnv('SIPROUTER_INITIAL_DEVICE_ADDRESS'), + extension: process.env.SIPROUTER_INITIAL_DEVICE_EXTENSION || '100', + }, + ], + incomingNumbers: [], + routing: { routes: [] }, + contacts: [], + faxboxes: [], + voiceboxes: [], + ivr: { + enabled: false, + entryMenuId: 'main-menu', + menus: [], + }, + }); +} - cfg.routing ??= { routes: [] }; - cfg.routing.routes ??= []; +export function maskConfig(configArg: IAppConfig): IAppConfig { + return { + ...configArg, + providers: configArg.providers?.map((providerArg) => ({ + ...providerArg, + password: providerArg.password ? '••••••' : providerArg.password, + })) || [], + }; +} - cfg.contacts ??= []; - for (const c of cfg.contacts) { - c.starred ??= false; - } +export function applyConfigUpdates(configArg: IAppConfig, updatesArg: any): IAppConfig { + const cfg = JSON.parse(JSON.stringify(configArg)) as IAppConfig; - cfg.faxboxes ??= []; - for (const fb of cfg.faxboxes) { - fb.enabled ??= true; - fb.maxMessages ??= 50; - } - - // Voicebox defaults. - cfg.voiceboxes ??= []; - for (const vb of cfg.voiceboxes) { - vb.enabled ??= true; - vb.noAnswerTimeoutSec ??= 25; - vb.maxRecordingSec ??= 120; - vb.maxMessages ??= 50; - vb.greetingVoice ??= 'af_bella'; - } - - // IVR defaults. - if (cfg.ivr) { - cfg.ivr.enabled ??= false; - cfg.ivr.menus ??= []; - for (const menu of cfg.ivr.menus) { - menu.timeoutSec ??= 5; - menu.maxRetries ??= 3; - menu.entries ??= []; + if (updatesArg.providers) { + for (const up of updatesArg.providers) { + const existing = cfg.providers?.find((p: any) => p.id === up.id); + if (existing) { + if (up.displayName !== undefined) existing.displayName = up.displayName; + if (up.password && up.password !== '••••••') existing.password = up.password; + if (up.domain !== undefined) existing.domain = up.domain; + if (up.outboundProxy !== undefined) existing.outboundProxy = up.outboundProxy; + if (up.username !== undefined) existing.username = up.username; + if (up.registerIntervalSec !== undefined) existing.registerIntervalSec = up.registerIntervalSec; + if (up.codecs !== undefined) existing.codecs = up.codecs; + if (up.quirks !== undefined) existing.quirks = up.quirks; + } } } - return cfg; + if (updatesArg.addProvider) { + cfg.providers ??= []; + cfg.providers.push(updatesArg.addProvider); + } + + if (updatesArg.removeProvider) { + cfg.providers = (cfg.providers || []).filter((p: any) => p.id !== updatesArg.removeProvider); + if (cfg.routing?.routes) { + cfg.routing.routes = cfg.routing.routes.filter((r: any) => + r.match?.sourceProvider !== updatesArg.removeProvider && + r.action?.provider !== updatesArg.removeProvider + ); + } + } + + if (updatesArg.devices) { + for (const ud of updatesArg.devices) { + const existing = cfg.devices?.find((d: any) => d.id === ud.id); + if (existing && ud.displayName !== undefined) existing.displayName = ud.displayName; + } + } + if (updatesArg.incomingNumbers !== undefined) cfg.incomingNumbers = updatesArg.incomingNumbers; + if (updatesArg.routing?.routes) cfg.routing.routes = updatesArg.routing.routes; + if (updatesArg.contacts !== undefined) cfg.contacts = updatesArg.contacts; + if (updatesArg.faxboxes !== undefined) cfg.faxboxes = updatesArg.faxboxes; + if (updatesArg.voiceboxes !== undefined) cfg.voiceboxes = updatesArg.voiceboxes; + if (updatesArg.ivr !== undefined) cfg.ivr = updatesArg.ivr; + + return normalizeConfig(cfg); } // Route resolution, pattern matching, and provider/device lookup diff --git a/ts/faxbox.ts b/ts/faxbox.ts index fb48541..af120c3 100644 --- a/ts/faxbox.ts +++ b/ts/faxbox.ts @@ -1,6 +1,9 @@ import fs from 'node:fs'; +import * as fsPromises from 'node:fs/promises'; import path from 'node:path'; +import type { SiprouterStorage } from './storage.ts'; + export interface IFaxBoxConfig { id: string; enabled: boolean; @@ -13,6 +16,7 @@ export interface IFaxMessage { callerNumber?: string; timestamp: number; fileName: string; + objectKey?: string; completionCode?: number | null; completionLabel?: string | null; pageCount?: number; @@ -21,24 +25,28 @@ export interface IFaxMessage { export class FaxBoxManager { private boxes = new Map(); + private messagesByBox = new Map(); private readonly basePath: string; private readonly log: (msg: string) => void; + private readonly storage: SiprouterStorage; - constructor(log: (msg: string) => void) { + constructor(log: (msg: string) => void, storageArg: SiprouterStorage) { this.basePath = path.join(process.cwd(), '.nogit', 'fax', 'inboxes'); this.log = log; + this.storage = storageArg; } - init(faxBoxConfigs: IFaxBoxConfig[]): void { + async init(faxBoxConfigs: IFaxBoxConfig[]): Promise { this.boxes.clear(); for (const cfg of faxBoxConfigs) { cfg.enabled ??= true; cfg.maxMessages ??= 50; this.boxes.set(cfg.id, cfg); + this.messagesByBox.set(cfg.id, await this.loadMessages(cfg.id)); } - fs.mkdirSync(this.basePath, { recursive: true }); + await fsPromises.mkdir(this.basePath, { recursive: true }); this.log(`[faxbox] initialized ${this.boxes.size} fax box(es)`); } @@ -50,7 +58,13 @@ export class FaxBoxManager { return path.join(this.basePath, boxId); } - addMessage( + async prepareOutboundFaxFile(filePathArg: string): Promise { + const localPath = path.isAbsolute(filePathArg) ? filePathArg : path.join(process.cwd(), filePathArg); + await fsPromises.access(localPath); + return localPath; + } + + async addMessage( boxId: string, info: { callerNumber?: string; @@ -60,90 +74,124 @@ export class FaxBoxManager { pageCount?: number; bitRate?: number; }, - ): void { + ): Promise { + const id = crypto.randomUUID(); + const localPath = path.isAbsolute(info.fileName) ? info.fileName : path.join(process.cwd(), info.fileName); + const objectKey = await this.storage.putFileObject(`fax/inboxes/${boxId}/${id}.tif`, localPath); + const msg: IFaxMessage = { - id: crypto.randomUUID(), + id, boxId, callerNumber: info.callerNumber, timestamp: Date.now(), - fileName: path.basename(info.fileName), + fileName: path.basename(localPath), + objectKey, completionCode: info.completionCode ?? null, completionLabel: info.completionLabel ?? null, pageCount: info.pageCount, bitRate: info.bitRate, }; - this.saveMessage(msg); - } - saveMessage(msg: IFaxMessage): void { - const boxDir = this.getBoxDir(msg.boxId); - fs.mkdirSync(boxDir, { recursive: true }); - - const messages = this.loadMessages(msg.boxId); + const messages = this.getMessages(boxId); messages.unshift(msg); - - const box = this.boxes.get(msg.boxId); - const maxMessages = box?.maxMessages ?? 50; - while (messages.length > maxMessages) { - const old = messages.pop()!; - const oldPath = path.join(boxDir, old.fileName); - try { - if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); - } catch {} - } - - this.writeMessages(msg.boxId, messages); + await this.enforceLimit(boxId, messages); + await this.writeMessages(boxId, messages); + await fsPromises.rm(localPath, { force: true }).catch(() => {}); this.log(`[faxbox] saved fax ${msg.id} in box "${msg.boxId}" (${msg.fileName})`); } getMessages(boxId: string): IFaxMessage[] { - return this.loadMessages(boxId); + return [...(this.messagesByBox.get(boxId) || [])]; } getMessage(boxId: string, messageId: string): IFaxMessage | null { - return this.loadMessages(boxId).find((m) => m.id === messageId) ?? null; + const messages = this.messagesByBox.get(boxId) || []; + return messages.find((m) => m.id === messageId) ?? null; } - getMessageFilePath(boxId: string, messageId: string): string | null { + async getMessageFilePath(boxId: string, messageId: string): Promise { const msg = this.getMessage(boxId, messageId); if (!msg) return null; + if (msg.objectKey) { + return await this.storage.getObjectAsCachedFile(msg.objectKey, msg.fileName); + } const filePath = path.join(this.getBoxDir(boxId), msg.fileName); return fs.existsSync(filePath) ? filePath : null; } - deleteMessage(boxId: string, messageId: string): boolean { - const messages = this.loadMessages(boxId); + async deleteMessage(boxId: string, messageId: string): Promise { + const messages = this.messagesByBox.get(boxId) || []; const idx = messages.findIndex((m) => m.id === messageId); if (idx === -1) return false; const msg = messages[idx]; - const filePath = path.join(this.getBoxDir(boxId), msg.fileName); - try { - if (fs.existsSync(filePath)) fs.unlinkSync(filePath); - } catch {} + await this.storage.removeObject(msg.objectKey); + if (!msg.objectKey) { + await fsPromises.rm(path.join(this.getBoxDir(boxId), msg.fileName), { force: true }).catch(() => {}); + } messages.splice(idx, 1); - this.writeMessages(boxId, messages); + await this.writeMessages(boxId, messages); return true; } - private messagesPath(boxId: string): string { - return path.join(this.getBoxDir(boxId), 'messages.json'); + private async enforceLimit(boxId: string, messages: IFaxMessage[]): Promise { + const box = this.boxes.get(boxId); + const maxMessages = box?.maxMessages ?? 50; + while (messages.length > maxMessages) { + const old = messages.pop()!; + await this.storage.removeObject(old.objectKey); + if (!old.objectKey) { + await fsPromises.rm(path.join(this.getBoxDir(boxId), old.fileName), { force: true }).catch(() => {}); + } + } } - private loadMessages(boxId: string): IFaxMessage[] { - const filePath = this.messagesPath(boxId); + private async loadMessages(boxId: string): Promise { + const storedMessages = await this.storage.getFaxMessages(boxId); + if (storedMessages.length) return await this.ensureMessageObjects(boxId, storedMessages); + + const filePath = path.join(this.getBoxDir(boxId), 'messages.json'); try { if (!fs.existsSync(filePath)) return []; - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as IFaxMessage[]; + const raw = await fsPromises.readFile(filePath, 'utf8'); + const legacyMessages = await this.ensureMessageObjects(boxId, JSON.parse(raw) as IFaxMessage[]); + await this.storage.writeFaxMessages(boxId, legacyMessages); + return legacyMessages; } catch { return []; } } - private writeMessages(boxId: string, messages: IFaxMessage[]): void { - const boxDir = this.getBoxDir(boxId); - fs.mkdirSync(boxDir, { recursive: true }); - fs.writeFileSync(this.messagesPath(boxId), JSON.stringify(messages, null, 2), 'utf8'); + private async ensureMessageObjects(boxId: string, messages: IFaxMessage[]): Promise { + let changed = false; + + for (const msg of messages) { + if (!msg.id) { + msg.id = crypto.randomUUID(); + changed = true; + } + if (msg.objectKey) continue; + + const localPath = path.isAbsolute(msg.fileName) ? msg.fileName : path.join(this.getBoxDir(boxId), msg.fileName); + if (!fs.existsSync(localPath)) continue; + + const extension = path.extname(localPath) || '.tif'; + msg.objectKey = await this.storage.putFileObject(`fax/inboxes/${boxId}/${msg.id}${extension}`, localPath); + msg.fileName = path.basename(localPath); + changed = true; + } + + if (changed) { + await this.storage.writeFaxMessages(boxId, messages); + this.log(`[faxbox] migrated legacy messages for box "${boxId}" to smartbucket`); + } + + return messages; + } + + private async writeMessages(boxId: string, messages: IFaxMessage[]): Promise { + this.messagesByBox.set(boxId, [...messages]); + await this.storage.writeFaxMessages(boxId, messages); } } diff --git a/ts/faxjobs.ts b/ts/faxjobs.ts index 426091e..1af7a45 100644 --- a/ts/faxjobs.ts +++ b/ts/faxjobs.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import type { SiprouterStorage } from './storage.ts'; import type { IFaxCompletedEvent, IFaxFailedEvent, @@ -16,6 +17,7 @@ export interface IFaxJob { status: 'dialing' | 'started' | 'completed' | 'failed'; transport?: 'audio' | 't38'; filePath?: string; + objectKey?: string; codec?: string; remoteMedia?: string; success?: boolean; @@ -28,25 +30,21 @@ export interface IFaxJob { } export class FaxJobManager { - private readonly basePath: string; - private readonly jobsPath: string; + private jobs: IFaxJob[] = []; private readonly log: (msg: string) => void; + private readonly storage: SiprouterStorage; - constructor(log: (msg: string) => void) { - this.basePath = path.join(process.cwd(), '.nogit', 'fax'); - this.jobsPath = path.join(this.basePath, 'jobs.json'); + constructor(log: (msg: string) => void, storageArg: SiprouterStorage) { this.log = log; + this.storage = storageArg; } - init(): void { - fs.mkdirSync(this.basePath, { recursive: true }); - if (!fs.existsSync(this.jobsPath)) { - this.writeJobs([]); - } + async init(): Promise { + this.jobs = await this.storage.getFaxJobs(); } - noteDialing(callId: string, number: string, providerId: string): void { - const jobs = this.loadJobs(); + async noteDialing(callId: string, number: string, providerId: string): Promise { + const jobs = this.jobs; const now = Date.now(); const existing = jobs.find((job) => job.callId === callId); if (existing) { @@ -65,62 +63,61 @@ export class FaxJobManager { updatedAt: now, }); } - this.writeJobs(jobs); + await this.writeJobs(); } - noteStarted(event: IFaxStartedEvent): void { - const jobs = this.loadJobs(); + async noteStarted(event: IFaxStartedEvent): Promise { const now = Date.now(); - const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); + const job = this.getOrCreateJob(event.call_id, event.direction, now); job.status = 'started'; job.transport = event.transport; job.filePath = event.file_path; + await this.ensureOutboundFileObject(job, event.file_path); job.codec = event.codec; job.remoteMedia = event.remote_media; job.updatedAt = now; - this.writeJobs(jobs); + await this.writeJobs(); } - noteCompleted(event: IFaxCompletedEvent): void { - const jobs = this.loadJobs(); + async noteCompleted(event: IFaxCompletedEvent): Promise { const now = Date.now(); - const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); + const job = this.getOrCreateJob(event.call_id, event.direction, now); job.status = 'completed'; job.transport = event.transport; job.filePath = event.file_path; + await this.ensureOutboundFileObject(job, event.file_path); job.codec = event.codec; job.success = event.success; job.completionCode = event.completion_code ?? null; job.completionLabel = event.completion_label ?? null; job.stats = event.stats; job.updatedAt = now; - this.writeJobs(jobs); + await this.writeJobs(); } - noteFailed(event: IFaxFailedEvent): void { - const jobs = this.loadJobs(); + async noteFailed(event: IFaxFailedEvent): Promise { const now = Date.now(); - const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); + const job = this.getOrCreateJob(event.call_id, event.direction, now); job.status = 'failed'; job.transport = event.transport; job.filePath = event.file_path; + await this.ensureOutboundFileObject(job, event.file_path); job.error = event.error; job.success = false; job.updatedAt = now; - this.writeJobs(jobs); + await this.writeJobs(); } getJobs(): IFaxJob[] { - return this.loadJobs(); + return [...this.jobs]; } private getOrCreateJob( - jobs: IFaxJob[], callId: string, direction: 'outbound' | 'inbound', now: number, ): IFaxJob { - let job = jobs.find((entry) => entry.callId === callId); + let job = this.jobs.find((entry) => entry.callId === callId); if (!job) { job = { id: callId, @@ -130,24 +127,23 @@ export class FaxJobManager { createdAt: now, updatedAt: now, }; - jobs.unshift(job); + this.jobs.unshift(job); } return job; } - private loadJobs(): IFaxJob[] { - try { - const content = fs.readFileSync(this.jobsPath, 'utf8'); - const parsed = JSON.parse(content); - return Array.isArray(parsed) ? parsed as IFaxJob[] : []; - } catch { - return []; - } + private async ensureOutboundFileObject(jobArg: IFaxJob, filePathArg: string | undefined): Promise { + if (jobArg.direction !== 'outbound' || jobArg.objectKey || !filePathArg) return; + + const localPath = path.isAbsolute(filePathArg) ? filePathArg : path.join(process.cwd(), filePathArg); + if (!fs.existsSync(localPath)) return; + + const extension = path.extname(localPath) || '.tif'; + jobArg.objectKey = await this.storage.putFileObject(`fax/outbound/${jobArg.callId}${extension}`, localPath); } - private writeJobs(jobs: IFaxJob[]): void { - fs.mkdirSync(this.basePath, { recursive: true }); - fs.writeFileSync(this.jobsPath, JSON.stringify(jobs, null, 2)); - this.log(`[fax] persisted ${jobs.length} job(s)`); + private async writeJobs(): Promise { + await this.storage.writeFaxJobs(this.jobs); + this.log(`[fax] persisted ${this.jobs.length} job(s)`); } } diff --git a/ts/frontend.ts b/ts/frontend.ts index 8c33a7b..e41a3cf 100644 --- a/ts/frontend.ts +++ b/ts/frontend.ts @@ -11,19 +11,19 @@ import path from 'node:path'; import http from 'node:http'; import https from 'node:https'; import { WebSocketServer, WebSocket } from 'ws'; +import { maskConfig, type IAppConfig } from './config.ts'; import type { FaxBoxManager } from './faxbox.ts'; import type { FaxJobManager } from './faxjobs.ts'; import { handleWebRtcSignaling } from './webrtcbridge.ts'; import type { VoiceboxManager } from './voicebox.ts'; -const CONFIG_PATH = path.join(process.cwd(), '.nogit', 'config.json'); - interface IHandleRequestContext { getStatus: () => unknown; + getConfig: () => IAppConfig; + updateConfig: (updatesArg: any) => Promise; log: (msg: string) => void; onStartCall: (number: string, deviceId?: string, providerId?: string) => Promise<{ id: string } | null>; onHangupCall: (callId: string) => boolean; - onConfigSaved?: () => void | Promise; faxBoxManager?: FaxBoxManager; faxJobManager?: FaxJobManager; voiceboxManager?: VoiceboxManager; @@ -112,7 +112,7 @@ async function handleRequest( res: http.ServerResponse, context: IHandleRequestContext, ): Promise { - const { getStatus, log, onStartCall, onHangupCall, onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager } = context; + const { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager } = context; const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); const method = req.method || 'GET'; @@ -156,13 +156,16 @@ async function handleRequest( try { const body = await readJsonBody(req); const number = body?.number; - const filePath = body?.filePath; + let filePath = body?.filePath; if (!number || typeof number !== 'string') { return sendJson(res, { ok: false, error: 'missing "number" field' }, 400); } if (!filePath || typeof filePath !== 'string') { return sendJson(res, { ok: false, error: 'missing "filePath" field' }, 400); } + if (faxBoxManager) { + filePath = await faxBoxManager.prepareOutboundFaxFile(filePath); + } const { sendFax } = await import('./proxybridge.ts'); const callId = await sendFax(number, filePath, body?.providerId); if (callId) { @@ -191,7 +194,7 @@ async function handleRequest( const faxFileMatch = url.pathname.match(/^\/api\/fax\/inboxes\/([^/]+)\/([^/]+)\/file$/); if (faxFileMatch && method === 'GET' && faxBoxManager) { const [, boxId, msgId] = faxFileMatch; - const filePath = faxBoxManager.getMessageFilePath(boxId, msgId); + const filePath = await faxBoxManager.getMessageFilePath(boxId, msgId); if (!filePath) return sendJson(res, { ok: false, error: 'not found' }, 404); const stat = fs.statSync(filePath); res.writeHead(200, { @@ -207,7 +210,7 @@ async function handleRequest( const faxDeleteMatch = url.pathname.match(/^\/api\/fax\/inboxes\/([^/]+)\/([^/]+)$/); if (faxDeleteMatch && method === 'DELETE' && faxBoxManager) { const [, boxId, msgId] = faxDeleteMatch; - return sendJson(res, { ok: faxBoxManager.deleteMessage(boxId, msgId) }); + return sendJson(res, { ok: await faxBoxManager.deleteMessage(boxId, msgId) }); } // API: add a SIP device to a call (mid-call INVITE to desk phone). @@ -272,10 +275,7 @@ async function handleRequest( // API: get config (sans passwords). if (url.pathname === '/api/config' && method === 'GET') { try { - const raw = fs.readFileSync(CONFIG_PATH, 'utf8'); - const cfg = JSON.parse(raw); - const safe = { ...cfg, providers: cfg.providers?.map((p: any) => ({ ...p, password: '••••••' })) }; - return sendJson(res, safe); + return sendJson(res, maskConfig(getConfig())); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 500); } @@ -285,65 +285,9 @@ async function handleRequest( if (url.pathname === '/api/config' && method === 'POST') { try { const updates = await readJsonBody(req); - const raw = fs.readFileSync(CONFIG_PATH, 'utf8'); - const cfg = JSON.parse(raw); - - // Update existing providers. - if (updates.providers) { - for (const up of updates.providers) { - const existing = cfg.providers?.find((p: any) => p.id === up.id); - if (existing) { - if (up.displayName !== undefined) existing.displayName = up.displayName; - if (up.password && up.password !== '••••••') existing.password = up.password; - if (up.domain !== undefined) existing.domain = up.domain; - if (up.outboundProxy !== undefined) existing.outboundProxy = up.outboundProxy; - if (up.username !== undefined) existing.username = up.username; - if (up.registerIntervalSec !== undefined) existing.registerIntervalSec = up.registerIntervalSec; - if (up.codecs !== undefined) existing.codecs = up.codecs; - if (up.quirks !== undefined) existing.quirks = up.quirks; - } - } - } - - // Add a new provider. - if (updates.addProvider) { - cfg.providers ??= []; - cfg.providers.push(updates.addProvider); - } - - // Remove a provider. - if (updates.removeProvider) { - cfg.providers = (cfg.providers || []).filter((p: any) => p.id !== updates.removeProvider); - // Clean up routing references — remove routes that reference this provider. - if (cfg.routing?.routes) { - cfg.routing.routes = cfg.routing.routes.filter((r: any) => - r.match?.sourceProvider !== updates.removeProvider && - r.action?.provider !== updates.removeProvider - ); - } - } - - if (updates.devices) { - for (const ud of updates.devices) { - const existing = cfg.devices?.find((d: any) => d.id === ud.id); - if (existing && ud.displayName !== undefined) existing.displayName = ud.displayName; - } - } - if (updates.incomingNumbers !== undefined) cfg.incomingNumbers = updates.incomingNumbers; - if (updates.routing) { - if (updates.routing.routes) { - cfg.routing.routes = updates.routing.routes; - } - } - if (updates.contacts !== undefined) cfg.contacts = updates.contacts; - if (updates.faxboxes !== undefined) cfg.faxboxes = updates.faxboxes; - if (updates.voiceboxes !== undefined) cfg.voiceboxes = updates.voiceboxes; - if (updates.ivr !== undefined) cfg.ivr = updates.ivr; - - fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n'); - log('[config] updated config.json'); - await onConfigSaved?.(); - return sendJson(res, { ok: true }); + const config = await updateConfig(updates); + log('[config] updated smartdata config'); + return sendJson(res, { ok: true, config: maskConfig(config) }); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 400); } @@ -367,7 +311,7 @@ async function handleRequest( const vmAudioMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)\/audio$/); if (vmAudioMatch && method === 'GET' && voiceboxManager) { const [, boxId, msgId] = vmAudioMatch; - const audioPath = voiceboxManager.getMessageAudioPath(boxId, msgId); + const audioPath = await voiceboxManager.getMessageAudioPath(boxId, msgId); if (!audioPath) return sendJson(res, { ok: false, error: 'not found' }, 404); const stat = fs.statSync(audioPath); res.writeHead(200, { @@ -383,14 +327,14 @@ async function handleRequest( const vmHeardMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)\/heard$/); if (vmHeardMatch && method === 'POST' && voiceboxManager) { const [, boxId, msgId] = vmHeardMatch; - return sendJson(res, { ok: voiceboxManager.markHeard(boxId, msgId) }); + return sendJson(res, { ok: await voiceboxManager.markHeard(boxId, msgId) }); } // API: voicemail - delete message. const vmDeleteMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)$/); if (vmDeleteMatch && method === 'DELETE' && voiceboxManager) { const [, boxId, msgId] = vmDeleteMatch; - return sendJson(res, { ok: voiceboxManager.deleteMessage(boxId, msgId) }); + return sendJson(res, { ok: await voiceboxManager.deleteMessage(boxId, msgId) }); } // Static files. @@ -428,10 +372,11 @@ export function initWebUi( const { port, getStatus, + getConfig, + updateConfig, log, onStartCall, onHangupCall, - onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager, @@ -453,12 +398,12 @@ export function initWebUi( const cert = fs.readFileSync(certPath, 'utf8'); const key = fs.readFileSync(keyPath, 'utf8'); server = https.createServer({ cert, key }, (req, res) => - handleRequest(req, res, { getStatus, log, onStartCall, onHangupCall, onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }), + handleRequest(req, res, { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }), ); useTls = true; } catch { server = http.createServer((req, res) => - handleRequest(req, res, { getStatus, log, onStartCall, onHangupCall, onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }), + handleRequest(req, res, { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }), ); } diff --git a/ts/plugins.ts b/ts/plugins.ts new file mode 100644 index 0000000..d82d5c7 --- /dev/null +++ b/ts/plugins.ts @@ -0,0 +1,4 @@ +import * as smartbucket from '@push.rocks/smartbucket'; +import * as smartdata from '@push.rocks/smartdata'; + +export { smartbucket, smartdata }; diff --git a/ts/runtime/proxy-events.ts b/ts/runtime/proxy-events.ts index 6edd46d..73fd778 100644 --- a/ts/runtime/proxy-events.ts +++ b/ts/runtime/proxy-events.ts @@ -102,7 +102,8 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO statusStore.noteOutboundCallStarted(data); if (data.ring_browsers === false) { - faxJobManager.noteDialing(data.call_id, data.number, data.provider_id); + void faxJobManager.noteDialing(data.call_id, data.number, data.provider_id) + .catch((error) => log(`[fax] persist dialing failed: ${error instanceof Error ? error.message : String(error)}`)); } if (data.ring_browsers === false) { @@ -218,12 +219,12 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO onProxyEvent('recording_done', (data) => { const boxId = data.voicebox_id || 'default'; log(`[voicemail] recording done: ${data.file_path} (${data.duration_ms}ms) box=${boxId} caller=${data.caller_number}`); - voiceboxManager.addMessage(boxId, { + void voiceboxManager.addMessage(boxId, { callerNumber: data.caller_number || 'Unknown', callerName: null, fileName: data.file_path, durationMs: data.duration_ms, - }); + }).catch((error) => log(`[voicemail] persist failed: ${error instanceof Error ? error.message : String(error)}`)); }); onProxyEvent('voicemail_error', (data) => { @@ -231,24 +232,24 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO }); onProxyEvent('fax_started', (data) => { - faxJobManager.noteStarted(data); + void faxJobManager.noteStarted(data).catch((error) => log(`[fax] persist start failed: ${error instanceof Error ? error.message : String(error)}`)); log(`[fax] started: call=${data.call_id} leg=${data.leg_id} ${data.direction}/${data.transport} codec=${data.codec || '?'} file=${data.file_path}`); }); onProxyEvent('fax_completed', (data) => { - faxJobManager.noteCompleted(data); + void faxJobManager.noteCompleted(data).catch((error) => log(`[fax] persist completion failed: ${error instanceof Error ? error.message : String(error)}`)); log( `[fax] completed: call=${data.call_id} leg=${data.leg_id} success=${data.success} pagesTx=${data.stats.pages_tx} bitrate=${data.stats.bit_rate} completion=${data.completion_label || data.completion_code || 'unknown'}`, ); if (data.direction === 'inbound' && data.success && data.fax_box_id) { - faxBoxManager.addMessage(data.fax_box_id, { + void faxBoxManager.addMessage(data.fax_box_id, { callerNumber: data.caller_number, fileName: data.file_path, completionCode: data.completion_code, completionLabel: data.completion_label, pageCount: data.stats.pages_rx || data.stats.pages_tx, bitRate: data.stats.bit_rate, - }); + }).catch((error) => log(`[fax] persist inbox failed: ${error instanceof Error ? error.message : String(error)}`)); } if (data.direction === 'outbound' || data.fax_box_id) { void hangupCall(data.call_id); @@ -256,7 +257,7 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO }); onProxyEvent('fax_failed', (data) => { - faxJobManager.noteFailed(data); + void faxJobManager.noteFailed(data).catch((error) => log(`[fax] persist failure failed: ${error instanceof Error ? error.message : String(error)}`)); log(`[fax] failed: call=${data.call_id} leg=${data.leg_id} error=${data.error}`); if (data.direction === 'outbound' || data.fax_box_id) { void hangupCall(data.call_id); diff --git a/ts/sipproxy.ts b/ts/sipproxy.ts index 21c3d37..bd5f823 100644 --- a/ts/sipproxy.ts +++ b/ts/sipproxy.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { loadConfig, type IAppConfig } from './config.ts'; +import { applyConfigUpdates, type IAppConfig } from './config.ts'; import { FaxBoxManager } from './faxbox.ts'; import { FaxJobManager } from './faxjobs.ts'; import { broadcastWs, initWebUi } from './frontend.ts'; @@ -27,24 +27,21 @@ import { } from './proxybridge.ts'; import { registerProxyEventHandlers } from './runtime/proxy-events.ts'; import { StatusStore } from './runtime/status-store.ts'; +import { SiprouterStorage } from './storage.ts'; import { WebRtcLinkManager, type IProviderMediaInfo } from './runtime/webrtc-linking.ts'; -let appConfig: IAppConfig = loadConfig(); +let appConfig: IAppConfig; const LOG_PATH = path.join(process.cwd(), 'sip_trace.log'); const startTime = Date.now(); const instanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; -const statusStore = new StatusStore(appConfig); -const webRtcLinks = new WebRtcLinkManager(); -const faxBoxManager = new FaxBoxManager(log); -const faxJobManager = new FaxJobManager(log); -const voiceboxManager = new VoiceboxManager(log); - -faxBoxManager.init(appConfig.faxboxes ?? []); -faxJobManager.init(); -voiceboxManager.init(appConfig.voiceboxes ?? []); -initWebRtcSignaling({ log }); +const storage = new SiprouterStorage(log); +let statusStore: StatusStore; +let webRtcLinks: WebRtcLinkManager; +let faxBoxManager: FaxBoxManager; +let faxJobManager: FaxJobManager; +let voiceboxManager: VoiceboxManager; function now(): string { return new Date().toISOString().replace('T', ' ').slice(0, 19); @@ -96,12 +93,12 @@ async function configureRuntime(config: IAppConfig): Promise { async function reloadConfig(): Promise { try { const previousConfig = appConfig; - const nextConfig = loadConfig(); + const nextConfig = await storage.getAppConfig(); appConfig = nextConfig; statusStore.updateConfig(nextConfig); - faxBoxManager.init(nextConfig.faxboxes ?? []); - voiceboxManager.init(nextConfig.voiceboxes ?? []); + await faxBoxManager.init(nextConfig.faxboxes ?? []); + await voiceboxManager.init(nextConfig.voiceboxes ?? []); if (nextConfig.proxy.lanPort !== previousConfig.proxy.lanPort) { log('[config] proxy.lanPort changed; restart required for SIP socket rebinding'); @@ -121,6 +118,13 @@ async function reloadConfig(): Promise { } } +async function updateConfig(updatesArg: any): Promise { + const nextConfig = applyConfigUpdates(appConfig, updatesArg); + await storage.writeAppConfig(nextConfig); + await reloadConfig(); + return appConfig; +} + async function startProxyEngine(): Promise { const started = await initProxyEngine(log); if (!started) { @@ -155,77 +159,101 @@ async function startProxyEngine(): Promise { log(`proxy engine started | LAN ${appConfig.proxy.lanIp}:${appConfig.proxy.lanPort} | providers: ${providerList} | devices: ${deviceList}`); } -initWebUi({ - port: appConfig.proxy.webUiPort, - getStatus, - log, - onStartCall: async (number, deviceId, providerId) => { - log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`); - const callId = await makeCall(number, deviceId, providerId); - if (!callId) { - log(`[dashboard] call failed for ${number}`); - return null; - } - log(`[dashboard] call started: ${callId}`); - statusStore.noteDashboardCallStarted(callId, number, providerId); - return { id: callId }; - }, - onHangupCall: (callId) => { - void hangupCall(callId); - return true; - }, - onConfigSaved: reloadConfig, - faxBoxManager, - faxJobManager, - voiceboxManager, - onWebRtcOffer: async (sessionId, sdp, ws) => { - log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`); - if (!sdp || typeof sdp !== 'string' || sdp.length < 10) { - log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`); - return; - } +async function main(): Promise { + await storage.init(); + appConfig = await storage.getAppConfig(); - log(`[webrtc] sending offer to Rust (${sdp.length}b)...`); - const result = await webrtcOffer(sessionId, sdp); - log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`); - if (result?.sdp) { - ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp })); - log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`); - return; - } + statusStore = new StatusStore(appConfig); + webRtcLinks = new WebRtcLinkManager(); + faxBoxManager = new FaxBoxManager(log, storage); + faxJobManager = new FaxJobManager(log, storage); + voiceboxManager = new VoiceboxManager(log, storage); - log('[webrtc] ERROR: no answer SDP from Rust'); - }, - onWebRtcIce: async (sessionId, candidate) => { - await webrtcIce(sessionId, candidate as Parameters[1]); - }, - onWebRtcClose: async (sessionId) => { - webRtcLinks.removeSession(sessionId); - await webrtcClose(sessionId); - }, - onWebRtcAccept: (callId, sessionId) => { - log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`); + await faxBoxManager.init(appConfig.faxboxes ?? []); + await faxJobManager.init(); + await voiceboxManager.init(appConfig.voiceboxes ?? []); + initWebRtcSignaling({ log }); - const pendingMedia = webRtcLinks.acceptCall(callId, sessionId); - if (pendingMedia) { - requestWebRtcLink(callId, sessionId, pendingMedia); - return; - } + initWebUi({ + port: appConfig.proxy.webUiPort, + getStatus, + getConfig: () => appConfig, + updateConfig, + log, + onStartCall: async (number, deviceId, providerId) => { + log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`); + const callId = await makeCall(number, deviceId, providerId); + if (!callId) { + log(`[dashboard] call failed for ${number}`); + return null; + } + log(`[dashboard] call started: ${callId}`); + statusStore.noteDashboardCallStarted(callId, number, providerId); + return { id: callId }; + }, + onHangupCall: (callId) => { + void hangupCall(callId); + return true; + }, + faxBoxManager, + faxJobManager, + voiceboxManager, + onWebRtcOffer: async (sessionId, sdp, ws) => { + log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`); + if (!sdp || typeof sdp !== 'string' || sdp.length < 10) { + log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`); + return; + } - log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`); - }, + log(`[webrtc] sending offer to Rust (${sdp.length}b)...`); + const result = await webrtcOffer(sessionId, sdp); + log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`); + if (result?.sdp) { + ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp })); + log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`); + return; + } + + log('[webrtc] ERROR: no answer SDP from Rust'); + }, + onWebRtcIce: async (sessionId, candidate) => { + await webrtcIce(sessionId, candidate as Parameters[1]); + }, + onWebRtcClose: async (sessionId) => { + webRtcLinks.removeSession(sessionId); + await webrtcClose(sessionId); + }, + onWebRtcAccept: (callId, sessionId) => { + log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`); + + const pendingMedia = webRtcLinks.acceptCall(callId, sessionId); + if (pendingMedia) { + requestWebRtcLink(callId, sessionId, pendingMedia); + return; + } + + log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`); + }, + }); + + await startProxyEngine(); +} + +void main().catch((error) => { + log(`[FATAL] ${errorMessage(error)}`); + process.exit(1); }); -void startProxyEngine(); - process.on('SIGINT', () => { log('SIGINT, exiting'); shutdownProxyEngine(); + void storage.close(); process.exit(0); }); process.on('SIGTERM', () => { log('SIGTERM, exiting'); shutdownProxyEngine(); + void storage.close(); process.exit(0); }); diff --git a/ts/storage.ts b/ts/storage.ts new file mode 100644 index 0000000..27e054f --- /dev/null +++ b/ts/storage.ts @@ -0,0 +1,250 @@ +import fs from 'node:fs'; +import * as fsPromises from 'node:fs/promises'; +import path from 'node:path'; + +import * as plugins from './plugins.ts'; +import { + createInitialConfigFromEnv, + normalizeConfig, + type IAppConfig, +} from './config.ts'; +import type { IFaxMessage } from './faxbox.ts'; +import type { IFaxJob } from './faxjobs.ts'; +import type { IVoicemailMessage } from './voicebox.ts'; + +interface ISiprouterDataStore { + appConfig: IAppConfig; + faxJobs: IFaxJob[]; + faxMessagesByBox: Record; + voicemailMessagesByBox: Record; +} + +type TLogFunction = (messageArg: string) => void; + +const legacyConfigPath = path.join(process.cwd(), '.nogit', 'config.json'); + +function requiredEnv(keysArg: string[]): string { + for (const key of keysArg) { + const value = process.env[key]; + if (value) return value; + } + throw new Error(`Missing required environment variable: ${keysArg.join(' or ')}`); +} + +function optionalNumber(valueArg: string | undefined, fallbackArg?: number): number | undefined { + if (!valueArg) return fallbackArg; + const parsed = Number(valueArg); + return Number.isFinite(parsed) ? parsed : fallbackArg; +} + +function optionalBoolean(valueArg: string | undefined, fallbackArg?: boolean): boolean | undefined { + if (valueArg === undefined) return fallbackArg; + return !['0', 'false', 'no', 'off'].includes(valueArg.toLowerCase()); +} + +function normalizeObjectKey(keyArg: string): string { + const normalizedKey = keyArg.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/'); + if (normalizedKey.split('/').includes('..')) { + throw new Error(`Invalid object key: ${keyArg}`); + } + return normalizedKey; +} + +export class SiprouterStorage { + private db!: InstanceType; + private store!: any; + private bucket!: any; + private readonly cacheDir = path.join(process.cwd(), '.nogit', 'cache'); + private readonly log: TLogFunction; + + constructor(logArg: TLogFunction) { + this.log = logArg; + } + + public async init(): Promise { + this.db = new plugins.smartdata.SmartdataDb(this.getMongoDescriptor() as any); + await this.db.init(); + this.store = await this.db.createEasyStore('siprouter-data'); + + const smartBucket = new plugins.smartbucket.SmartBucket(this.getS3Descriptor() as any); + const bucketName = requiredEnv(['SIPROUTER_S3_BUCKET', 'S3_BUCKET']); + this.bucket = await smartBucket.bucketExists(bucketName) + ? await smartBucket.getBucketByName(bucketName) + : await smartBucket.createBucket(bucketName); + + await fsPromises.mkdir(this.cacheDir, { recursive: true }); + this.log('[storage] smartdata and smartbucket initialized'); + } + + public async close(): Promise { + if (this.db) { + await this.db.close(); + } + } + + public async getAppConfig(): Promise { + const storedConfig = await this.readKey('appConfig'); + if (storedConfig) { + return normalizeConfig(storedConfig); + } + + const legacyConfig = await this.readLegacyConfig(); + const initialConfig = legacyConfig || createInitialConfigFromEnv(); + await this.writeAppConfig(initialConfig); + this.log(legacyConfig ? '[storage] imported legacy .nogit/config.json into smartdata' : '[storage] created initial smartdata config'); + return initialConfig; + } + + public async writeAppConfig(configArg: IAppConfig): Promise { + await this.writeKey('appConfig', normalizeConfig(configArg)); + } + + public async getFaxJobs(): Promise { + return (await this.readKey('faxJobs')) || []; + } + + public async writeFaxJobs(jobsArg: IFaxJob[]): Promise { + await this.writeKey('faxJobs', jobsArg); + } + + public async getVoicemailMessages(boxIdArg: string): Promise { + const allMessages = (await this.readKey('voicemailMessagesByBox')) || {}; + return allMessages[boxIdArg] || []; + } + + public async writeVoicemailMessages(boxIdArg: string, messagesArg: IVoicemailMessage[]): Promise { + const allMessages = (await this.readKey('voicemailMessagesByBox')) || {}; + allMessages[boxIdArg] = messagesArg; + await this.writeKey('voicemailMessagesByBox', allMessages); + } + + public async getFaxMessages(boxIdArg: string): Promise { + const allMessages = (await this.readKey('faxMessagesByBox')) || {}; + return allMessages[boxIdArg] || []; + } + + public async writeFaxMessages(boxIdArg: string, messagesArg: IFaxMessage[]): Promise { + const allMessages = (await this.readKey('faxMessagesByBox')) || {}; + allMessages[boxIdArg] = messagesArg; + await this.writeKey('faxMessagesByBox', allMessages); + } + + public async putFileObject(objectKeyArg: string, filePathArg: string): Promise { + const objectKey = normalizeObjectKey(objectKeyArg); + const contents = await fsPromises.readFile(filePathArg); + await this.bucket.fastPut({ path: objectKey, contents, overwrite: true }); + await this.removeCachedObject(objectKey); + return objectKey; + } + + public async putBufferObject(objectKeyArg: string, bufferArg: Buffer): Promise { + const objectKey = normalizeObjectKey(objectKeyArg); + await this.bucket.fastPut({ path: objectKey, contents: bufferArg, overwrite: true }); + await this.removeCachedObject(objectKey); + return objectKey; + } + + public async getObjectAsCachedFile(objectKeyArg: string, fileNameArg?: string): Promise { + const objectKey = normalizeObjectKey(objectKeyArg); + const cachePath = this.getCachePath(objectKey); + try { + if (fs.existsSync(cachePath)) { + return cachePath; + } + const contents = await this.bucket.fastGet({ path: objectKey }); + await fsPromises.mkdir(path.dirname(cachePath), { recursive: true }); + await fsPromises.writeFile(cachePath, contents); + return cachePath; + } catch { + if (fileNameArg) { + const fallbackPath = path.join(this.cacheDir, path.basename(fileNameArg)); + return fs.existsSync(fallbackPath) ? fallbackPath : null; + } + return null; + } + } + + public async removeObject(objectKeyArg: string | undefined): Promise { + if (!objectKeyArg) return; + const objectKey = normalizeObjectKey(objectKeyArg); + try { + await this.bucket.fastRemove({ path: objectKey }); + } catch { + // Missing objects are harmless during metadata cleanup. + } + await this.removeCachedObject(objectKey); + } + + private getCachePath(objectKeyArg: string): string { + return path.join(this.cacheDir, normalizeObjectKey(objectKeyArg)); + } + + private async removeCachedObject(objectKeyArg: string): Promise { + await fsPromises.rm(this.getCachePath(objectKeyArg), { force: true }).catch(() => {}); + } + + private async readLegacyConfig(): Promise { + try { + const raw = await fsPromises.readFile(legacyConfigPath, 'utf8'); + return normalizeConfig(JSON.parse(raw) as IAppConfig); + } catch { + return null; + } + } + + private async readKey(keyArg: TKey): Promise { + try { + return await this.store.readKey(keyArg) as ISiprouterDataStore[TKey] | undefined; + } catch { + return undefined; + } + } + + private async writeKey( + keyArg: TKey, + valueArg: ISiprouterDataStore[TKey], + ): Promise { + await this.store.writeKey(keyArg, valueArg); + } + + private getMongoDescriptor(): Record { + const mongoDbUrl = requiredEnv([ + 'SIPROUTER_MONGODB_URL', + 'MONGODB_URI', + 'MONGODB_URL', + ]); + const descriptor: Record = { + mongoDbUrl, + mongoDbName: process.env.SIPROUTER_MONGODB_NAME || process.env.MONGODB_DATABASE || process.env.MONGODB_NAME || 'siprouter', + }; + + const mongoDbUser = process.env.SIPROUTER_MONGODB_USER || process.env.MONGODB_USERNAME || process.env.MONGODB_USER; + const mongoDbPass = process.env.SIPROUTER_MONGODB_PASS || process.env.MONGODB_PASSWORD || process.env.MONGODB_PASS; + if (mongoDbUser) descriptor.mongoDbUser = mongoDbUser; + if (mongoDbPass) descriptor.mongoDbPass = mongoDbPass; + return descriptor; + } + + private getS3Descriptor(): Record { + const rawEndpoint = requiredEnv(['SIPROUTER_S3_ENDPOINT', 'S3_ENDPOINT', 'AWS_ENDPOINT_URL']); + let endpoint = rawEndpoint; + let port = optionalNumber(process.env.SIPROUTER_S3_PORT || process.env.S3_PORT); + let useSsl = optionalBoolean(process.env.SIPROUTER_S3_USESSL || process.env.S3_USESSL || process.env.S3_USE_SSL); + + if (/^https?:\/\//.test(rawEndpoint)) { + const url = new URL(rawEndpoint); + endpoint = url.hostname; + port = url.port ? Number(url.port) : port; + useSsl = url.protocol === 'https:'; + } + + return { + endpoint, + accessKey: requiredEnv(['SIPROUTER_S3_ACCESS_KEY', 'S3_ACCESS_KEY', 'AWS_ACCESS_KEY_ID']), + accessSecret: requiredEnv(['SIPROUTER_S3_SECRET_KEY', 'S3_SECRET_KEY', 'AWS_SECRET_ACCESS_KEY']), + region: process.env.SIPROUTER_S3_REGION || process.env.S3_REGION || process.env.AWS_REGION || 'us-east-1', + ...(port ? { port } : {}), + ...(useSsl !== undefined ? { useSsl } : {}), + }; + } +} diff --git a/ts/voicebox.ts b/ts/voicebox.ts index c54b0d3..f8f9978 100644 --- a/ts/voicebox.ts +++ b/ts/voicebox.ts @@ -1,22 +1,12 @@ /** - * VoiceboxManager — manages voicemail boxes, message storage, and MWI. - * - * Each voicebox corresponds to a device/extension. Messages are stored - * as WAV files with JSON metadata in .nogit/voicemail/{boxId}/. - * - * Supports: - * - Per-box configurable TTS greetings (text + voice) or uploaded WAV - * - Message CRUD: save, list, mark heard, delete - * - Unheard count for MWI (Message Waiting Indicator) - * - Storage limit (max messages per box) + * VoiceboxManager — manages voicemail boxes, message metadata, and audio objects. */ import fs from 'node:fs'; +import * as fsPromises from 'node:fs/promises'; import path from 'node:path'; -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- +import type { SiprouterStorage } from './storage.ts'; export interface IVoiceboxConfig { /** Unique ID — typically matches device ID or extension. */ @@ -27,11 +17,9 @@ export interface IVoiceboxConfig { greetingText?: string; /** Kokoro TTS voice ID for the greeting (default 'af_bella'). */ greetingVoice?: string; - /** Path to uploaded WAV greeting (overrides TTS). */ + /** Path to cached uploaded WAV greeting (overrides TTS). */ greetingWavPath?: string; - /** Seconds to wait before routing to voicemail. Defaults to 25 when - * absent — both the config loader and `VoiceboxManager.init` apply - * the default via `??=`. */ + /** Seconds to wait before routing to voicemail. */ noAnswerTimeoutSec?: number; /** Maximum recording duration in seconds. Defaults to 120. */ maxRecordingSec?: number; @@ -52,112 +40,80 @@ export interface IVoicemailMessage { timestamp: number; /** Duration in milliseconds. */ durationMs: number; - /** Relative path to the WAV file (within the box directory). */ + /** Display file name. */ fileName: string; + /** SmartBucket object key for the WAV payload. */ + objectKey?: string; /** Whether the message has been listened to. */ heard: boolean; } -// Default greeting text when no custom text is configured. const DEFAULT_GREETING = 'The person you are trying to reach is not available. Please leave a message after the tone.'; -// --------------------------------------------------------------------------- -// VoiceboxManager -// --------------------------------------------------------------------------- - export class VoiceboxManager { private boxes = new Map(); - private basePath: string; - private log: (msg: string) => void; + private messagesByBox = new Map(); + private readonly basePath: string; + private readonly log: (msg: string) => void; + private readonly storage: SiprouterStorage; - constructor(log: (msg: string) => void) { + constructor(log: (msg: string) => void, storageArg: SiprouterStorage) { this.basePath = path.join(process.cwd(), '.nogit', 'voicemail'); this.log = log; + this.storage = storageArg; } - // ------------------------------------------------------------------------- - // Initialization - // ------------------------------------------------------------------------- - - /** - * Load voicebox configurations from the app config. - */ - init(voiceboxConfigs: IVoiceboxConfig[]): void { + async init(voiceboxConfigs: IVoiceboxConfig[]): Promise { this.boxes.clear(); for (const cfg of voiceboxConfigs) { - // Apply defaults. cfg.noAnswerTimeoutSec ??= 25; cfg.maxRecordingSec ??= 120; cfg.maxMessages ??= 50; cfg.greetingVoice ??= 'af_bella'; - this.boxes.set(cfg.id, cfg); + this.messagesByBox.set(cfg.id, await this.loadMessages(cfg.id)); } - // Ensure base directory exists. - fs.mkdirSync(this.basePath, { recursive: true }); - + await fsPromises.mkdir(this.basePath, { recursive: true }); this.log(`[voicebox] initialized ${this.boxes.size} voicebox(es)`); } - // ------------------------------------------------------------------------- - // Box management - // ------------------------------------------------------------------------- - - /** Get config for a specific voicebox. */ getBox(boxId: string): IVoiceboxConfig | null { return this.boxes.get(boxId) ?? null; } - /** Get all configured voicebox IDs. */ getBoxIds(): string[] { return [...this.boxes.keys()]; } - /** Get the greeting text for a voicebox. */ getGreetingText(boxId: string): string { const box = this.boxes.get(boxId); return box?.greetingText || DEFAULT_GREETING; } - /** Get the greeting voice for a voicebox. */ getGreetingVoice(boxId: string): string { const box = this.boxes.get(boxId); return box?.greetingVoice || 'af_bella'; } - /** Check if a voicebox has a custom WAV greeting. */ hasCustomGreetingWav(boxId: string): boolean { const box = this.boxes.get(boxId); if (!box?.greetingWavPath) return false; return fs.existsSync(box.greetingWavPath); } - /** Get the greeting WAV path (custom or null). */ getCustomGreetingWavPath(boxId: string): string | null { const box = this.boxes.get(boxId); if (!box?.greetingWavPath) return null; return fs.existsSync(box.greetingWavPath) ? box.greetingWavPath : null; } - /** Get the directory path for a voicebox. */ getBoxDir(boxId: string): string { return path.join(this.basePath, boxId); } - // ------------------------------------------------------------------------- - // Message CRUD - // ------------------------------------------------------------------------- - - /** - * Convenience wrapper around `saveMessage` — used by the `recording_done` - * event handler, which has a raw recording path + caller info and needs - * to persist metadata. Generates `id`, sets `timestamp = now`, defaults - * `heard = false`, and normalizes `fileName` to a basename (the WAV is - * expected to already live in the box's directory). - */ - addMessage( + async addMessage( boxId: string, info: { callerNumber: string; @@ -165,124 +121,87 @@ export class VoiceboxManager { fileName: string; durationMs: number; }, - ): void { + ): Promise { + const id = crypto.randomUUID(); + const localPath = path.isAbsolute(info.fileName) ? info.fileName : path.join(process.cwd(), info.fileName); + const objectKey = await this.storage.putFileObject(`voicemail/${boxId}/${id}.wav`, localPath); + const msg: IVoicemailMessage = { - id: crypto.randomUUID(), + id, boxId, callerNumber: info.callerNumber, callerName: info.callerName ?? undefined, timestamp: Date.now(), durationMs: info.durationMs, - fileName: path.basename(info.fileName), + fileName: path.basename(localPath), + objectKey, heard: false, }; - this.saveMessage(msg); - } - /** - * Save a new voicemail message. - * The WAV file should already exist at the expected path. - */ - saveMessage(msg: IVoicemailMessage): void { - const boxDir = this.getBoxDir(msg.boxId); - fs.mkdirSync(boxDir, { recursive: true }); + const messages = this.getMessages(boxId); + messages.unshift(msg); + await this.enforceLimit(boxId, messages); + await this.writeMessages(boxId, messages); - const messages = this.loadMessages(msg.boxId); - messages.unshift(msg); // newest first - - // Enforce max messages — delete oldest. - const box = this.boxes.get(msg.boxId); - const maxMessages = box?.maxMessages ?? 50; - while (messages.length > maxMessages) { - const old = messages.pop()!; - const oldPath = path.join(boxDir, old.fileName); - try { - if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); - } catch { /* best effort */ } - } - - this.writeMessages(msg.boxId, messages); + await fsPromises.rm(localPath, { force: true }).catch(() => {}); this.log(`[voicebox] saved message ${msg.id} in box "${msg.boxId}" (${msg.durationMs}ms from ${msg.callerNumber})`); } - /** - * List messages for a voicebox (newest first). - */ getMessages(boxId: string): IVoicemailMessage[] { - return this.loadMessages(boxId); + return [...(this.messagesByBox.get(boxId) || [])]; } - /** - * Get a single message by ID. - */ getMessage(boxId: string, messageId: string): IVoicemailMessage | null { - const messages = this.loadMessages(boxId); + const messages = this.messagesByBox.get(boxId) || []; return messages.find((m) => m.id === messageId) ?? null; } - /** - * Mark a message as heard. - */ - markHeard(boxId: string, messageId: string): boolean { - const messages = this.loadMessages(boxId); + async markHeard(boxId: string, messageId: string): Promise { + const messages = this.messagesByBox.get(boxId) || []; const msg = messages.find((m) => m.id === messageId); if (!msg) return false; msg.heard = true; - this.writeMessages(boxId, messages); + await this.writeMessages(boxId, messages); return true; } - /** - * Delete a message (both metadata and WAV file). - */ - deleteMessage(boxId: string, messageId: string): boolean { - const messages = this.loadMessages(boxId); + async deleteMessage(boxId: string, messageId: string): Promise { + const messages = this.messagesByBox.get(boxId) || []; const idx = messages.findIndex((m) => m.id === messageId); if (idx === -1) return false; const msg = messages[idx]; - const boxDir = this.getBoxDir(boxId); - const wavPath = path.join(boxDir, msg.fileName); + await this.storage.removeObject(msg.objectKey); + if (!msg.objectKey) { + await fsPromises.rm(path.join(this.getBoxDir(boxId), msg.fileName), { force: true }).catch(() => {}); + } - // Delete WAV file. - try { - if (fs.existsSync(wavPath)) fs.unlinkSync(wavPath); - } catch { /* best effort */ } - - // Remove from list and save. messages.splice(idx, 1); - this.writeMessages(boxId, messages); + await this.writeMessages(boxId, messages); this.log(`[voicebox] deleted message ${messageId} from box "${boxId}"`); return true; } - /** - * Get the full file path for a message's WAV file. - */ - getMessageAudioPath(boxId: string, messageId: string): string | null { + async getMessageAudioPath(boxId: string, messageId: string): Promise { const msg = this.getMessage(boxId, messageId); if (!msg) return null; + if (msg.objectKey) { + return await this.storage.getObjectAsCachedFile(msg.objectKey, msg.fileName); + } const filePath = path.join(this.getBoxDir(boxId), msg.fileName); return fs.existsSync(filePath) ? filePath : null; } - // ------------------------------------------------------------------------- - // Counts - // ------------------------------------------------------------------------- - - /** Get count of unheard messages for a voicebox. */ getUnheardCount(boxId: string): number { - const messages = this.loadMessages(boxId); + const messages = this.messagesByBox.get(boxId) || []; return messages.filter((m) => !m.heard).length; } - /** Get total message count for a voicebox. */ getTotalCount(boxId: string): number { - return this.loadMessages(boxId).length; + return (this.messagesByBox.get(boxId) || []).length; } - /** Get unheard counts for all voiceboxes. */ getAllUnheardCounts(): Record { const counts: Record = {}; for (const boxId of this.boxes.keys()) { @@ -291,55 +210,74 @@ export class VoiceboxManager { return counts; } - // ------------------------------------------------------------------------- - // Greeting management - // ------------------------------------------------------------------------- - - /** - * Save a custom greeting WAV file for a voicebox. - */ - saveCustomGreeting(boxId: string, wavData: Buffer): string { - const boxDir = this.getBoxDir(boxId); - fs.mkdirSync(boxDir, { recursive: true }); - const greetingPath = path.join(boxDir, 'greeting.wav'); - fs.writeFileSync(greetingPath, wavData); + async saveCustomGreeting(boxId: string, wavData: Buffer): Promise { + const objectKey = await this.storage.putBufferObject(`voicemail/${boxId}/greeting.wav`, wavData); + const greetingPath = await this.storage.getObjectAsCachedFile(objectKey, `voicemail-${boxId}-greeting.wav`); this.log(`[voicebox] saved custom greeting for box "${boxId}"`); - return greetingPath; + return greetingPath || ''; } - /** - * Delete the custom greeting for a voicebox (falls back to TTS). - */ - deleteCustomGreeting(boxId: string): void { - const boxDir = this.getBoxDir(boxId); - const greetingPath = path.join(boxDir, 'greeting.wav'); - try { - if (fs.existsSync(greetingPath)) fs.unlinkSync(greetingPath); - } catch { /* best effort */ } + async deleteCustomGreeting(boxId: string): Promise { + await this.storage.removeObject(`voicemail/${boxId}/greeting.wav`); } - // ------------------------------------------------------------------------- - // Internal: JSON persistence - // ------------------------------------------------------------------------- - - private messagesPath(boxId: string): string { - return path.join(this.getBoxDir(boxId), 'messages.json'); + private async enforceLimit(boxId: string, messages: IVoicemailMessage[]): Promise { + const box = this.boxes.get(boxId); + const maxMessages = box?.maxMessages ?? 50; + while (messages.length > maxMessages) { + const old = messages.pop()!; + await this.storage.removeObject(old.objectKey); + if (!old.objectKey) { + await fsPromises.rm(path.join(this.getBoxDir(boxId), old.fileName), { force: true }).catch(() => {}); + } + } } - private loadMessages(boxId: string): IVoicemailMessage[] { - const filePath = this.messagesPath(boxId); + private async loadMessages(boxId: string): Promise { + const storedMessages = await this.storage.getVoicemailMessages(boxId); + if (storedMessages.length) return await this.ensureMessageObjects(boxId, storedMessages); + + const filePath = path.join(this.getBoxDir(boxId), 'messages.json'); try { if (!fs.existsSync(filePath)) return []; - const raw = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(raw) as IVoicemailMessage[]; + const raw = await fsPromises.readFile(filePath, 'utf8'); + const legacyMessages = await this.ensureMessageObjects(boxId, JSON.parse(raw) as IVoicemailMessage[]); + await this.storage.writeVoicemailMessages(boxId, legacyMessages); + return legacyMessages; } catch { return []; } } - private writeMessages(boxId: string, messages: IVoicemailMessage[]): void { - const boxDir = this.getBoxDir(boxId); - fs.mkdirSync(boxDir, { recursive: true }); - fs.writeFileSync(this.messagesPath(boxId), JSON.stringify(messages, null, 2), 'utf8'); + private async ensureMessageObjects(boxId: string, messages: IVoicemailMessage[]): Promise { + let changed = false; + + for (const msg of messages) { + if (!msg.id) { + msg.id = crypto.randomUUID(); + changed = true; + } + if (msg.objectKey) continue; + + const localPath = path.isAbsolute(msg.fileName) ? msg.fileName : path.join(this.getBoxDir(boxId), msg.fileName); + if (!fs.existsSync(localPath)) continue; + + const extension = path.extname(localPath) || '.wav'; + msg.objectKey = await this.storage.putFileObject(`voicemail/${boxId}/${msg.id}${extension}`, localPath); + msg.fileName = path.basename(localPath); + changed = true; + } + + if (changed) { + await this.storage.writeVoicemailMessages(boxId, messages); + this.log(`[voicebox] migrated legacy messages for box "${boxId}" to smartbucket`); + } + + return messages; + } + + private async writeMessages(boxId: string, messages: IVoicemailMessage[]): Promise { + this.messagesByBox.set(boxId, [...messages]); + await this.storage.writeVoicemailMessages(boxId, messages); } }