switch to more deno functions and do other optimizations

This commit is contained in:
2026-03-30 18:15:27 -06:00
parent fedb9ca0c5
commit 219257fef2
12 changed files with 242 additions and 497 deletions

View File

@@ -6,11 +6,8 @@
"check": "deno check src/**/*.ts"
},
"imports": {
"express": "npm:express@4",
"cors": "npm:cors@2",
"helmet": "npm:helmet@8",
"hono": "jsr:@hono/hono",
"pino": "npm:pino@9",
"bcryptjs": "npm:bcryptjs@2",
"twilio": "npm:twilio@5",
"node-cron": "npm:node-cron@3",
"@db/sqlite": "jsr:@db/sqlite@0.12",

287
deno.lock generated
View File

@@ -3,6 +3,7 @@
"specifiers": {
"jsr:@db/sqlite@0.12": "0.12.0",
"jsr:@denosaurs/plug@1": "1.1.0",
"jsr:@hono/hono@*": "4.12.9",
"jsr:@std/assert@0.217": "0.217.0",
"jsr:@std/encoding@*": "1.0.10",
"jsr:@std/encoding@1": "1.0.10",
@@ -12,10 +13,6 @@
"jsr:@std/path@0.217": "0.217.0",
"jsr:@std/path@1": "1.1.3",
"jsr:@std/path@^1.1.3": "1.1.3",
"npm:bcryptjs@2": "2.4.3",
"npm:cors@2": "2.8.6",
"npm:express@4": "4.22.1",
"npm:helmet@8": "8.1.0",
"npm:node-cron@3": "3.0.3",
"npm:pino@9": "9.14.0",
"npm:twilio@5": "5.13.1"
@@ -37,6 +34,9 @@
"jsr:@std/path@1"
]
},
"@hono/hono@4.12.9": {
"integrity": "53c2b0a721c1d782e6a347ad5bf15a3235ff624b98859c21f779b7ee2f5e040e"
},
"@std/assert@0.217.0": {
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
},
@@ -73,22 +73,12 @@
"@pinojs/redact@0.4.0": {
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="
},
"accepts@1.3.8": {
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": [
"mime-types",
"negotiator"
]
},
"agent-base@6.0.2": {
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": [
"debug@4.4.3"
"debug"
]
},
"array-flatten@1.1.1": {
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"asynckit@0.4.0": {
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
@@ -103,32 +93,9 @@
"proxy-from-env"
]
},
"bcryptjs@2.4.3": {
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"body-parser@1.20.4": {
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"dependencies": [
"bytes",
"content-type",
"debug@2.6.9",
"depd",
"destroy",
"http-errors",
"iconv-lite",
"on-finished",
"qs",
"raw-body",
"type-is",
"unpipe"
]
},
"buffer-equal-constant-time@1.0.1": {
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"bytes@3.1.2": {
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"call-bind-apply-helpers@1.0.2": {
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": [
@@ -149,52 +116,18 @@
"delayed-stream"
]
},
"content-disposition@0.5.4": {
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": [
"safe-buffer"
]
},
"content-type@1.0.5": {
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"cookie-signature@1.0.7": {
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"cookie@0.7.2": {
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
},
"cors@2.8.6": {
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"dependencies": [
"object-assign",
"vary"
]
},
"dayjs@1.11.20": {
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="
},
"debug@2.6.9": {
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": [
"ms@2.0.0"
]
},
"debug@4.4.3": {
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": [
"ms@2.1.3"
"ms"
]
},
"delayed-stream@1.0.0": {
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd@2.0.0": {
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy@1.2.0": {
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"dunder-proto@1.0.1": {
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": [
@@ -209,12 +142,6 @@
"safe-buffer"
]
},
"ee-first@1.1.1": {
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"encodeurl@2.0.0": {
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"es-define-property@1.0.1": {
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
@@ -236,60 +163,6 @@
"hasown"
]
},
"escape-html@1.0.3": {
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"etag@1.8.1": {
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"express@4.22.1": {
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"dependencies": [
"accepts",
"array-flatten",
"body-parser",
"content-disposition",
"content-type",
"cookie",
"cookie-signature",
"debug@2.6.9",
"depd",
"encodeurl",
"escape-html",
"etag",
"finalhandler",
"fresh",
"http-errors",
"merge-descriptors",
"methods",
"on-finished",
"parseurl",
"path-to-regexp",
"proxy-addr",
"qs",
"range-parser",
"safe-buffer",
"send",
"serve-static",
"setprototypeof",
"statuses",
"type-is",
"utils-merge",
"vary"
]
},
"finalhandler@1.3.2": {
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"dependencies": [
"debug@2.6.9",
"encodeurl",
"escape-html",
"on-finished",
"parseurl",
"statuses",
"unpipe"
]
},
"follow-redirects@1.15.11": {
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
},
@@ -303,12 +176,6 @@
"mime-types"
]
},
"forwarded@0.2.0": {
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fresh@0.5.2": {
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"function-bind@1.1.2": {
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
@@ -352,38 +219,13 @@
"function-bind"
]
},
"helmet@8.1.0": {
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg=="
},
"http-errors@2.0.1": {
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dependencies": [
"depd",
"inherits",
"setprototypeof",
"statuses",
"toidentifier"
]
},
"https-proxy-agent@5.0.1": {
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": [
"agent-base",
"debug@4.4.3"
"debug"
]
},
"iconv-lite@0.4.24": {
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": [
"safer-buffer"
]
},
"inherits@2.0.4": {
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js@1.9.1": {
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"jsonwebtoken@9.0.3": {
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"dependencies": [
@@ -395,7 +237,7 @@
"lodash.isplainobject",
"lodash.isstring",
"lodash.once",
"ms@2.1.3",
"ms",
"semver"
]
},
@@ -438,15 +280,6 @@
"math-intrinsics@1.1.0": {
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"media-typer@0.3.0": {
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors@1.0.3": {
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
},
"methods@1.1.2": {
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
@@ -456,46 +289,21 @@
"mime-db"
]
},
"mime@1.6.0": {
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": true
},
"ms@2.0.0": {
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"negotiator@0.6.3": {
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"node-cron@3.0.3": {
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"dependencies": [
"uuid"
]
},
"object-assign@4.1.1": {
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect@1.13.4": {
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
},
"on-exit-leak-free@2.1.2": {
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="
},
"on-finished@2.4.1": {
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": [
"ee-first"
]
},
"parseurl@1.3.3": {
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp@0.1.13": {
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="
},
"pino-abstract-transport@2.0.0": {
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"dependencies": [
@@ -525,13 +333,6 @@
"process-warning@5.0.0": {
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="
},
"proxy-addr@2.0.7": {
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": [
"forwarded",
"ipaddr.js"
]
},
"proxy-from-env@2.1.0": {
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="
},
@@ -544,18 +345,6 @@
"quick-format-unescaped@4.0.4": {
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"range-parser@1.2.1": {
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body@2.5.3": {
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"dependencies": [
"bytes",
"http-errors",
"iconv-lite",
"unpipe"
]
},
"real-require@0.2.0": {
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="
},
@@ -565,9 +354,6 @@
"safe-stable-stringify@2.5.0": {
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"scmp@2.1.0": {
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==",
"deprecated": true
@@ -576,36 +362,6 @@
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"bin": true
},
"send@0.19.2": {
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"dependencies": [
"debug@2.6.9",
"depd",
"destroy",
"encodeurl",
"escape-html",
"etag",
"fresh",
"http-errors",
"mime",
"ms@2.1.3",
"on-finished",
"range-parser",
"statuses"
]
},
"serve-static@1.16.3": {
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"dependencies": [
"encodeurl",
"escape-html",
"parseurl",
"send"
]
},
"setprototypeof@1.2.0": {
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"side-channel-list@1.0.0": {
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dependencies": [
@@ -651,18 +407,12 @@
"split2@4.2.0": {
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
},
"statuses@2.0.2": {
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="
},
"thread-stream@3.1.0": {
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"dependencies": [
"real-require"
]
},
"toidentifier@1.0.1": {
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"twilio@5.13.1": {
"integrity": "sha512-sT+PkhptF4Mf7t8eXFFvPQx4w5VHnBIPXbltGPMFRe+R2GxfRdMuFbuNA/cEm0aQR6LFQOn33+fhClg+TjRVqQ==",
"dependencies": [
@@ -675,26 +425,10 @@
"xmlbuilder"
]
},
"type-is@1.6.18": {
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": [
"media-typer",
"mime-types"
]
},
"unpipe@1.0.0": {
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"utils-merge@1.0.1": {
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
},
"uuid@8.3.2": {
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": true
},
"vary@1.1.2": {
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"xmlbuilder@13.0.2": {
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="
}
@@ -702,11 +436,8 @@
"workspace": {
"dependencies": [
"jsr:@db/sqlite@0.12",
"jsr:@hono/hono@*",
"jsr:@std/encoding@*",
"npm:bcryptjs@2",
"npm:cors@2",
"npm:express@4",
"npm:helmet@8",
"npm:node-cron@3",
"npm:pino@9",
"npm:twilio@5"

View File

@@ -1,76 +1,63 @@
import express from "express";
import { Hono } from "hono";
import type { AppEnv } from "../../common/types.ts";
import { getDb } from "../../db.ts";
import { config } from "../../common/config.ts";
import l from "../../common/logger.ts";
import { authMiddleware } from "../middleware/auth.ts";
import { decodeBase64Url } from "@std/encoding/base64url";
class Controller {
// deno-lint-ignore no-explicit-any
async storeJwt(req: any, res: any): Promise<void> {
try {
const { jwt } = req.body;
if (!jwt) {
res.status(400).json({ error: { message: "jwt is required", code: "MISSING_FIELD" } });
return;
}
const router = new Hono<AppEnv>();
const issuerDid = res.locals.issuerDid;
const db = getDb();
router.use("*", authMiddleware);
// deno-lint-ignore no-explicit-any
const user = db.prepare("SELECT * FROM sms_user WHERE issuer_did = ?").get(issuerDid) as any;
if (!user) {
res.status(400).json({ error: { message: "No phone registration found. Register first.", code: "NOT_REGISTERED" } });
return;
}
if (!user.verified) {
res.status(400).json({ error: { message: "Phone not verified", code: "NOT_VERIFIED" } });
return;
}
const response = await fetch(`${config.endorserApiUrl}/api/v2/report/rateLimits`, {
headers: { Authorization: `Bearer ${jwt}` },
});
if (!response.ok) {
res.status(400).json({ error: { message: "Provided JWT is not valid", code: "INVALID_JWT" } });
return;
}
let jwtExpiresAt: string | null = null;
try {
const payloadBytes = decodeBase64Url(jwt.split(".")[1]);
const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
if (payload.exp) {
jwtExpiresAt = new Date(payload.exp * 1000).toISOString();
}
} catch {
// If we can't parse expiry, store without it
}
const now = new Date().toISOString();
db.prepare(
"UPDATE sms_user SET stored_jwt = ?, jwt_expires_at = ?, updated_at = ? WHERE id = ?"
).run(jwt, jwtExpiresAt, now, user.id);
res.status(200).json({
success: {
message: "JWT stored",
jwtExpiresAt,
},
});
} catch (err) {
l.error("storeJwt error: %s", err);
res.status(500).json({ error: { message: "Internal server error" } });
router.put("/jwt", async (c) => {
try {
const { jwt } = await c.req.json();
if (!jwt) {
return c.json({ error: { message: "jwt is required", code: "MISSING_FIELD" } }, 400);
}
const issuerDid = c.get("issuerDid");
const db = getDb();
const user = db.prepare("SELECT * FROM sms_user WHERE issuer_did = ?").get(issuerDid) as Record<string, unknown> | undefined;
if (!user) {
return c.json({ error: { message: "No phone registration found. Register first.", code: "NOT_REGISTERED" } }, 400);
}
if (!user.verified) {
return c.json({ error: { message: "Phone not verified", code: "NOT_VERIFIED" } }, 400);
}
const response = await fetch(`${config.endorserApiUrl}/api/v2/report/rateLimits`, {
headers: { Authorization: `Bearer ${jwt}` },
});
if (!response.ok) {
return c.json({ error: { message: "Provided JWT is not valid", code: "INVALID_JWT" } }, 400);
}
let jwtExpiresAt: string | null = null;
try {
const payloadBytes = decodeBase64Url(jwt.split(".")[1]);
const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
if (payload.exp) {
jwtExpiresAt = new Date(payload.exp * 1000).toISOString();
}
} catch {
// If we can't parse expiry, store without it
}
const now = new Date().toISOString();
db.prepare(
"UPDATE sms_user SET stored_jwt = ?, jwt_expires_at = ?, updated_at = ? WHERE id = ?"
).run(jwt, jwtExpiresAt, now, user.id as number);
return c.json({ success: { message: "JWT stored", jwtExpiresAt } });
} catch (err) {
l.error("storeJwt error: %s", err);
return c.json({ error: { message: "Internal server error" } }, 500);
}
}
const controller = new Controller();
const router = express.Router();
router.put("/jwt", authMiddleware, controller.storeJwt);
});
export default router;

View File

@@ -1,90 +1,78 @@
import express from "express";
import { Hono } from "hono";
import type { AppEnv } from "../../common/types.ts";
import registrationService from "../services/registration.service.ts";
import { sendSms } from "../services/sms-provider.ts";
import { getDb } from "../../db.ts";
import l from "../../common/logger.ts";
import { authMiddleware } from "../middleware/auth.ts";
class Controller {
// deno-lint-ignore no-explicit-any
async register(req: any, res: any): Promise<void> {
try {
const { phoneNumber } = req.body;
if (!phoneNumber) {
res.status(400).json({ error: { message: "phoneNumber is required", code: "MISSING_FIELD" } });
return;
}
const router = new Hono<AppEnv>();
const issuerDid = res.locals.issuerDid;
const { code } = await registrationService.register(phoneNumber, issuerDid);
router.use("*", authMiddleware);
const db = getDb();
const user = db.prepare("SELECT id FROM sms_user WHERE phone_number = ?").get(phoneNumber) as { id: number } | undefined;
await sendSms(
phoneNumber,
`Your Time Safari verification code is: ${code}. It expires in 15 minutes.`,
user?.id,
"verification"
);
res.status(200).json({ success: { message: "Verification code sent" } });
// deno-lint-ignore no-explicit-any
} catch (err: any) {
if (err.clientError) {
res.status(400).json({ error: err.clientError });
} else {
l.error("register error: %s", err);
res.status(500).json({ error: { message: "Internal server error" } });
}
router.post("/register", async (c) => {
try {
const { phoneNumber } = await c.req.json();
if (!phoneNumber) {
return c.json({ error: { message: "phoneNumber is required", code: "MISSING_FIELD" } }, 400);
}
}
// deno-lint-ignore no-explicit-any
verify(req: any, res: any): void {
try {
const { phoneNumber, code } = req.body;
if (!phoneNumber || !code) {
res.status(400).json({ error: { message: "phoneNumber and code are required", code: "MISSING_FIELD" } });
return;
}
const issuerDid = c.get("issuerDid");
const { code } = await registrationService.register(phoneNumber, issuerDid);
const result = registrationService.verify(phoneNumber, code);
res.status(200).json({ success: result });
// deno-lint-ignore no-explicit-any
} catch (err: any) {
if (err.clientError) {
res.status(400).json({ error: err.clientError });
} else {
l.error("verify error: %s", err);
res.status(500).json({ error: { message: "Internal server error" } });
}
const db = getDb();
const user = db.prepare("SELECT id FROM sms_user WHERE phone_number = ?").get(phoneNumber) as { id: number } | undefined;
await sendSms(
phoneNumber,
`Your Time Safari verification code is: ${code}. It expires in 15 minutes.`,
user?.id,
"verification"
);
return c.json({ success: { message: "Verification code sent" } });
} catch (err: unknown) {
const e = err as { clientError?: { message: string; code: string } };
if (e.clientError) {
return c.json({ error: e.clientError }, 400);
}
l.error("register error: %s", err);
return c.json({ error: { message: "Internal server error" } }, 500);
}
});
// deno-lint-ignore no-explicit-any
unregister(_req: any, res: any): void {
try {
const issuerDid = res.locals.issuerDid;
registrationService.unregister(issuerDid);
res.status(200).json({ success: { message: "Registration removed" } });
// deno-lint-ignore no-explicit-any
} catch (err: any) {
if (err.clientError) {
res.status(400).json({ error: err.clientError });
} else {
l.error("unregister error: %s", err);
res.status(500).json({ error: { message: "Internal server error" } });
}
router.post("/verify", async (c) => {
try {
const { phoneNumber, code } = await c.req.json();
if (!phoneNumber || !code) {
return c.json({ error: { message: "phoneNumber and code are required", code: "MISSING_FIELD" } }, 400);
}
const result = await registrationService.verify(phoneNumber, code);
return c.json({ success: result });
} catch (err: unknown) {
const e = err as { clientError?: { message: string; code: string } };
if (e.clientError) {
return c.json({ error: e.clientError }, 400);
}
l.error("verify error: %s", err);
return c.json({ error: { message: "Internal server error" } }, 500);
}
}
});
const controller = new Controller();
const router = express.Router();
router.post("/register", authMiddleware, controller.register);
router.post("/verify", authMiddleware, controller.verify);
router.delete("/register", authMiddleware, controller.unregister);
router.delete("/register", (c) => {
try {
const issuerDid = c.get("issuerDid");
registrationService.unregister(issuerDid);
return c.json({ success: { message: "Registration removed" } });
} catch (err: unknown) {
const e = err as { clientError?: { message: string; code: string } };
if (e.clientError) {
return c.json({ error: e.clientError }, 400);
}
l.error("unregister error: %s", err);
return c.json({ error: { message: "Internal server error" } }, 500);
}
});
export default router;

View File

@@ -1,3 +1,6 @@
import type { Next } from "hono";
import type { Context } from "hono";
import type { AppEnv } from "../../common/types.ts";
import { config } from "../../common/config.ts";
import l from "../../common/logger.ts";
@@ -16,12 +19,10 @@ function simpleHash(str: string): string {
return hash.toString(36);
}
// deno-lint-ignore no-explicit-any
export async function authMiddleware(req: any, res: any, next: any): Promise<void> {
const authHeader = req.headers.authorization;
export async function authMiddleware(c: Context<AppEnv>, next: Next): Promise<Response | void> {
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith(BEARER_PREFIX)) {
res.status(401).json({ error: { message: "Missing or invalid Authorization header" } });
return;
return c.json({ error: { message: "Missing or invalid Authorization header" } }, 401);
}
const token = authHeader.substring(BEARER_PREFIX.length);
@@ -29,7 +30,7 @@ export async function authMiddleware(req: any, res: any, next: any): Promise<voi
const cached = didCache.get(tokenHash);
if (cached && cached.expiresAt > Date.now()) {
res.locals.issuerDid = cached.did;
c.set("issuerDid", cached.did);
return next();
}
@@ -40,25 +41,23 @@ export async function authMiddleware(req: any, res: any, next: any): Promise<voi
if (!response.ok) {
l.info(`Auth rejected by endorser-ch: ${response.status}`);
res.status(401).json({ error: { message: "Authentication failed" } });
return;
return c.json({ error: { message: "Authentication failed" } }, 401);
}
// deno-lint-ignore no-explicit-any
const data = (await response.json()) as any;
const did = data.payload?.issuer || data.issuer;
const data = await response.json() as Record<string, unknown>;
const payload = data.payload as Record<string, unknown> | undefined;
const did = payload?.issuer || data.issuer;
if (!did) {
l.error("No DID found in endorser-ch response");
res.status(401).json({ error: { message: "Could not extract DID from token" } });
return;
return c.json({ error: { message: "Could not extract DID from token" } }, 401);
}
didCache.set(tokenHash, { did, expiresAt: Date.now() + CACHE_TTL_MS });
didCache.set(tokenHash, { did: did as string, expiresAt: Date.now() + CACHE_TTL_MS });
res.locals.issuerDid = did;
next();
c.set("issuerDid", did as string);
return next();
} catch (err) {
l.error("Error contacting endorser-ch for auth: %s", err);
res.status(502).json({ error: { message: "Auth service unavailable" } });
return c.json({ error: { message: "Auth service unavailable" } }, 502);
}
}

View File

@@ -1,6 +1,6 @@
import bcrypt from "bcryptjs";
import { getDb } from "../../db.ts";
import l from "../../common/logger.ts";
import { encodeBase64 } from "@std/encoding/base64";
const E164_US_REGEX = /^\+1[2-9]\d{9}$/;
const CODE_LENGTH = 6;
@@ -8,7 +8,8 @@ const CODE_EXPIRY_MINUTES = 15;
const MAX_CODES_PER_HOUR = 3;
const MAX_VERIFY_ATTEMPTS = 5;
const COOLDOWN_MINUTES = 60;
const BCRYPT_ROUNDS = 10;
const PBKDF2_ITERATIONS = 100_000;
const SALT_LENGTH = 16;
interface SmsUser {
id: number;
@@ -81,7 +82,7 @@ export class RegistrationService {
}
const code = generateCode();
const hashedCode = bcrypt.hashSync(code, BCRYPT_ROUNDS);
const hashedCode = await hashCode(code);
const expiry = new Date(
Date.now() + CODE_EXPIRY_MINUTES * 60 * 1000
).toISOString();
@@ -114,7 +115,7 @@ export class RegistrationService {
return { code };
}
verify(phoneNumber: string, code: string): { success: boolean } {
async verify(phoneNumber: string, code: string): Promise<{ success: boolean }> {
const db = getDb();
const now = new Date().toISOString();
const user = db.prepare("SELECT * FROM sms_user WHERE phone_number = ?").get(phoneNumber) as SmsUser | undefined;
@@ -141,7 +142,7 @@ export class RegistrationService {
db.prepare("UPDATE sms_user SET verification_attempts = verification_attempts + 1, updated_at = ? WHERE id = ?").run(now, user.id);
const match = bcrypt.compareSync(code, user.verification_code);
const match = await verifyCode(code, user.verification_code);
if (!match) {
throw { clientError: { message: "Invalid verification code", code: "INVALID_CODE" } };
}
@@ -184,10 +185,47 @@ export class RegistrationService {
}
function generateCode(): string {
const digits = Math.floor(Math.random() * 1_000_000)
.toString()
.padStart(CODE_LENGTH, "0");
return digits;
const arr = new Uint32Array(1);
crypto.getRandomValues(arr);
return (arr[0] % 1_000_000).toString().padStart(CODE_LENGTH, "0");
}
async function hashCode(code: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(code),
"PBKDF2",
false,
["deriveBits"]
);
const derived = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
key,
256
);
const saltB64 = encodeBase64(salt);
const hashB64 = encodeBase64(new Uint8Array(derived));
return `${saltB64}:${hashB64}`;
}
async function verifyCode(code: string, stored: string): Promise<boolean> {
const [saltB64, hashB64] = stored.split(":");
const salt = Uint8Array.from(atob(saltB64), (c) => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(code),
"PBKDF2",
false,
["deriveBits"]
);
const derived = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
key,
256
);
const derivedB64 = encodeBase64(new Uint8Array(derived));
return derivedB64 === hashB64;
}
export default new RegistrationService();

View File

@@ -14,13 +14,18 @@ class LogProvider implements SmsProvider {
}
}
async function createProvider(): Promise<SmsProvider> {
function getProvider(): SmsProvider {
if (config.twilioAccountSid && config.twilioAuthToken && config.twilioPhoneNumber) {
l.info("Using Twilio SMS provider");
const twilio = (await import("twilio")).default;
const client = twilio(config.twilioAccountSid, config.twilioAuthToken);
// Lazy-loading Twilio provider: only imports the SDK on first call
// deno-lint-ignore no-explicit-any
let client: any = null;
return {
async sendSms(to: string, body: string) {
if (!client) {
l.info("Initializing Twilio SMS provider");
const twilio = (await import("twilio")).default;
client = twilio(config.twilioAccountSid, config.twilioAuthToken);
}
const message = await client.messages.create({
to,
from: config.twilioPhoneNumber,
@@ -34,11 +39,7 @@ async function createProvider(): Promise<SmsProvider> {
return new LogProvider();
}
let provider: SmsProvider;
export async function initSmsProvider(): Promise<void> {
provider = await createProvider();
}
const provider = getProvider();
export async function sendSms(
to: string,

View File

@@ -1,18 +1,12 @@
import type { Hono } from "hono";
import registrationRouter from "../api/controllers/registration-router.ts";
import jwtRouter from "../api/controllers/jwt-router.ts";
// deno-lint-ignore no-explicit-any
export default function routes(app: any): void {
// deno-lint-ignore no-explicit-any
app.get("/api/sms/health", (_req: any, res: any) => {
res.json({ status: "ok" });
});
export default function routes(app: Hono): void {
app.get("/api/sms/health", (c) => c.json({ status: "ok" }));
app.use("/api/sms", registrationRouter);
app.use("/api/sms", jwtRouter);
app.route("/api/sms", registrationRouter);
app.route("/api/sms", jwtRouter);
// deno-lint-ignore no-explicit-any
app.use("*", (_req: any, res: any) => {
res.status(404).json({ error: { message: "Route not found" } });
});
app.notFound((c) => c.json({ error: { message: "Route not found" } }, 404));
}

View File

@@ -1,33 +1,28 @@
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
import l from "./logger.ts";
export default class Server {
// deno-lint-ignore no-explicit-any
private app: any;
export function createApp(): Hono {
const app = new Hono();
constructor() {
this.app = express();
this.app.use(helmet());
this.app.use(express.json({ limit: "40kb" }));
this.app.use(express.urlencoded({ extended: true }));
this.app.use(
cors({
allowedHeaders: ["Authorization", "Content-Type"],
})
);
}
// Request logging
app.use("*", async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
l.info({ method: c.req.method, path: c.req.path, status: c.res.status, ms }, "request");
});
// deno-lint-ignore no-explicit-any
router(routeSetup: (app: any) => void): Server {
routeSetup(this.app);
return this;
}
listen(port: number): void {
this.app.listen(port, () => {
l.info(`Server running on port ${port}`);
});
}
app.use("*", secureHeaders());
app.use("*", cors({
origin: "*",
allowHeaders: ["Authorization", "Content-Type"],
}));
return app;
}
export function listen(app: Hono, port: number): Deno.HttpServer {
l.info(`Server running on port ${port}`);
return Deno.serve({ port }, app.fetch);
}

5
src/common/types.ts Normal file
View File

@@ -0,0 +1,5 @@
export type AppEnv = {
Variables: {
issuerDid: string;
};
};

View File

@@ -15,6 +15,7 @@ export function initDb(): void {
db = new Database(config.smsDbPath);
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA foreign_keys = ON");
db.exec("PRAGMA busy_timeout = 5000");
runMigrations();
}

View File

@@ -1,13 +1,22 @@
import Server from "./common/server.ts";
import { createApp, listen } from "./common/server.ts";
import routes from "./common/routes.ts";
import { config } from "./common/config.ts";
import { initDb } from "./db.ts";
import { initSmsProvider } from "./api/services/sms-provider.ts";
import { initDb, closeDb } from "./db.ts";
import l from "./common/logger.ts";
initDb();
l.info("Database initialized");
await initSmsProvider();
const app = createApp();
routes(app);
const server = listen(app, config.port);
new Server().router(routes).listen(config.port);
const shutdown = () => {
l.info("Shutting down...");
server.shutdown();
closeDb();
Deno.exit(0);
};
Deno.addSignalListener("SIGTERM", shutdown);
Deno.addSignalListener("SIGINT", shutdown);