Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
Cargo.lock +1041 −5
| @@ -3,6 +3,12 @@ | |||
| 3 | version = 4 | 3 | version = 4 |
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "allocator-api2" | ||
| 7 | version = "0.2.21" | ||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | ||
| 10 | |||
| 11 | [[package]] | ||
| 6 | name = "anstream" | 12 | name = "anstream" |
| 7 | version = "1.0.0" | 13 | version = "1.0.0" |
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | 14 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -63,6 +69,15 @@ name = "api" | |||
| 63 | version = "0.1.0" | 69 | version = "0.1.0" |
| 64 | 70 | ||
| 65 | [[package]] | 71 | [[package]] |
| 72 | name = "atoi" | ||
| 73 | version = "2.0.0" | ||
| 74 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 75 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" | ||
| 76 | dependencies = [ | ||
| 77 | "num-traits", | ||
| 78 | ] | ||
| 79 | |||
| 80 | [[package]] | ||
| 66 | name = "atomic" | 81 | name = "atomic" |
| 67 | version = "0.6.1" | 82 | version = "0.6.1" |
| 68 | source = "registry+https://github.com/rust-lang/crates.io-index" | 83 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -76,10 +91,49 @@ name = "auth" | |||
| 76 | version = "0.1.0" | 91 | version = "0.1.0" |
| 77 | 92 | ||
| 78 | [[package]] | 93 | [[package]] |
| 94 | name = "autocfg" | ||
| 95 | version = "1.5.1" | ||
| 96 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 97 | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | ||
| 98 | |||
| 99 | [[package]] | ||
| 100 | name = "base64" | ||
| 101 | version = "0.22.1" | ||
| 102 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 103 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | ||
| 104 | |||
| 105 | [[package]] | ||
| 79 | name = "bitflags" | 106 | name = "bitflags" |
| 80 | version = "2.13.1" | 107 | version = "2.13.1" |
| 81 | source = "registry+https://github.com/rust-lang/crates.io-index" | 108 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 82 | checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" | 109 | checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" |
| 110 | dependencies = [ | ||
| 111 | "serde_core", | ||
| 112 | ] | ||
| 113 | |||
| 114 | [[package]] | ||
| 115 | name = "block-buffer" | ||
| 116 | version = "0.10.4" | ||
| 117 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 118 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||
| 119 | dependencies = [ | ||
| 120 | "generic-array", | ||
| 121 | ] | ||
| 122 | |||
| 123 | [[package]] | ||
| 124 | name = "block-buffer" | ||
| 125 | version = "0.12.1" | ||
| 126 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 127 | checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" | ||
| 128 | dependencies = [ | ||
| 129 | "hybrid-array", | ||
| 130 | ] | ||
| 131 | |||
| 132 | [[package]] | ||
| 133 | name = "bumpalo" | ||
| 134 | version = "3.20.3" | ||
| 135 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 136 | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | ||
| 83 | 137 | ||
| 84 | [[package]] | 138 | [[package]] |
| 85 | name = "bytemuck" | 139 | name = "bytemuck" |
| @@ -88,12 +142,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 88 | checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" | 142 | checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" |
| 89 | 143 | ||
| 90 | [[package]] | 144 | [[package]] |
| 145 | name = "byteorder" | ||
| 146 | version = "1.5.0" | ||
| 147 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 148 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | ||
| 149 | |||
| 150 | [[package]] | ||
| 151 | name = "bytes" | ||
| 152 | version = "1.12.1" | ||
| 153 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 154 | checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" | ||
| 155 | |||
| 156 | [[package]] | ||
| 157 | name = "cc" | ||
| 158 | version = "1.4.0" | ||
| 159 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 160 | checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" | ||
| 161 | dependencies = [ | ||
| 162 | "find-msvc-tools", | ||
| 163 | "shlex", | ||
| 164 | ] | ||
| 165 | |||
| 166 | [[package]] | ||
| 91 | name = "cfg-if" | 167 | name = "cfg-if" |
| 92 | version = "1.0.4" | 168 | version = "1.0.4" |
| 93 | source = "registry+https://github.com/rust-lang/crates.io-index" | 169 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 94 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | 170 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" |
| 95 | 171 | ||
| 96 | [[package]] | 172 | [[package]] |
| 173 | name = "chacha20" | ||
| 174 | version = "0.10.1" | ||
| 175 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 176 | checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" | ||
| 177 | dependencies = [ | ||
| 178 | "cfg-if", | ||
| 179 | "cpufeatures 0.3.0", | ||
| 180 | "rand_core 0.10.1", | ||
| 181 | ] | ||
| 182 | |||
| 183 | [[package]] | ||
| 97 | name = "clap" | 184 | name = "clap" |
| 98 | version = "4.6.4" | 185 | version = "4.6.4" |
| 99 | source = "registry+https://github.com/rust-lang/crates.io-index" | 186 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -144,12 +231,27 @@ dependencies = [ | |||
| 144 | ] | 231 | ] |
| 145 | 232 | ||
| 146 | [[package]] | 233 | [[package]] |
| 234 | name = "cmov" | ||
| 235 | version = "0.5.4" | ||
| 236 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 237 | checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" | ||
| 238 | |||
| 239 | [[package]] | ||
| 147 | name = "colorchoice" | 240 | name = "colorchoice" |
| 148 | version = "1.0.5" | 241 | version = "1.0.5" |
| 149 | source = "registry+https://github.com/rust-lang/crates.io-index" | 242 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 150 | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" | 243 | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" |
| 151 | 244 | ||
| 152 | [[package]] | 245 | [[package]] |
| 246 | name = "concurrent-queue" | ||
| 247 | version = "2.5.0" | ||
| 248 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 249 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" | ||
| 250 | dependencies = [ | ||
| 251 | "crossbeam-utils", | ||
| 252 | ] | ||
| 253 | |||
| 254 | [[package]] | ||
| 153 | name = "config" | 255 | name = "config" |
| 154 | version = "0.1.0" | 256 | version = "0.1.0" |
| 155 | dependencies = [ | 257 | dependencies = [ |
| @@ -161,6 +263,103 @@ dependencies = [ | |||
| 161 | ] | 263 | ] |
| 162 | 264 | ||
| 163 | [[package]] | 265 | [[package]] |
| 266 | name = "cpufeatures" | ||
| 267 | version = "0.2.17" | ||
| 268 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 269 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | ||
| 270 | dependencies = [ | ||
| 271 | "libc", | ||
| 272 | ] | ||
| 273 | |||
| 274 | [[package]] | ||
| 275 | name = "cpufeatures" | ||
| 276 | version = "0.3.0" | ||
| 277 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 278 | checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" | ||
| 279 | dependencies = [ | ||
| 280 | "libc", | ||
| 281 | ] | ||
| 282 | |||
| 283 | [[package]] | ||
| 284 | name = "crc" | ||
| 285 | version = "3.4.0" | ||
| 286 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 287 | checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" | ||
| 288 | dependencies = [ | ||
| 289 | "crc-catalog", | ||
| 290 | ] | ||
| 291 | |||
| 292 | [[package]] | ||
| 293 | name = "crc-catalog" | ||
| 294 | version = "2.5.0" | ||
| 295 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 296 | checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" | ||
| 297 | |||
| 298 | [[package]] | ||
| 299 | name = "crossbeam-queue" | ||
| 300 | version = "0.3.13" | ||
| 301 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 302 | checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" | ||
| 303 | dependencies = [ | ||
| 304 | "crossbeam-utils", | ||
| 305 | ] | ||
| 306 | |||
| 307 | [[package]] | ||
| 308 | name = "crossbeam-utils" | ||
| 309 | version = "0.8.22" | ||
| 310 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 311 | checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" | ||
| 312 | |||
| 313 | [[package]] | ||
| 314 | name = "crypto-common" | ||
| 315 | version = "0.1.6" | ||
| 316 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 317 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | ||
| 318 | dependencies = [ | ||
| 319 | "generic-array", | ||
| 320 | "typenum", | ||
| 321 | ] | ||
| 322 | |||
| 323 | [[package]] | ||
| 324 | name = "crypto-common" | ||
| 325 | version = "0.2.2" | ||
| 326 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 327 | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" | ||
| 328 | dependencies = [ | ||
| 329 | "hybrid-array", | ||
| 330 | ] | ||
| 331 | |||
| 332 | [[package]] | ||
| 333 | name = "ctutils" | ||
| 334 | version = "0.4.2" | ||
| 335 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 336 | checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" | ||
| 337 | dependencies = [ | ||
| 338 | "cmov", | ||
| 339 | ] | ||
| 340 | |||
| 341 | [[package]] | ||
| 342 | name = "digest" | ||
| 343 | version = "0.10.7" | ||
| 344 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 345 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||
| 346 | dependencies = [ | ||
| 347 | "block-buffer 0.10.4", | ||
| 348 | "crypto-common 0.1.6", | ||
| 349 | ] | ||
| 350 | |||
| 351 | [[package]] | ||
| 352 | name = "digest" | ||
| 353 | version = "0.11.3" | ||
| 354 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 355 | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" | ||
| 356 | dependencies = [ | ||
| 357 | "block-buffer 0.12.1", | ||
| 358 | "crypto-common 0.2.2", | ||
| 359 | "ctutils", | ||
| 360 | ] | ||
| 361 | |||
| 362 | [[package]] | ||
| 164 | name = "displaydoc" | 363 | name = "displaydoc" |
| 165 | version = "0.2.6" | 364 | version = "0.2.6" |
| 166 | source = "registry+https://github.com/rust-lang/crates.io-index" | 365 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -172,6 +371,21 @@ dependencies = [ | |||
| 172 | ] | 371 | ] |
| 173 | 372 | ||
| 174 | [[package]] | 373 | [[package]] |
| 374 | name = "dotenvy" | ||
| 375 | version = "0.15.7" | ||
| 376 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 377 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" | ||
| 378 | |||
| 379 | [[package]] | ||
| 380 | name = "either" | ||
| 381 | version = "1.17.0" | ||
| 382 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 383 | checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" | ||
| 384 | dependencies = [ | ||
| 385 | "serde", | ||
| 386 | ] | ||
| 387 | |||
| 388 | [[package]] | ||
| 175 | name = "equivalent" | 389 | name = "equivalent" |
| 176 | version = "1.0.2" | 390 | version = "1.0.2" |
| 177 | source = "registry+https://github.com/rust-lang/crates.io-index" | 391 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -188,6 +402,27 @@ dependencies = [ | |||
| 188 | ] | 402 | ] |
| 189 | 403 | ||
| 190 | [[package]] | 404 | [[package]] |
| 405 | name = "etcetera" | ||
| 406 | version = "0.11.0" | ||
| 407 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 408 | checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" | ||
| 409 | dependencies = [ | ||
| 410 | "cfg-if", | ||
| 411 | "windows-sys", | ||
| 412 | ] | ||
| 413 | |||
| 414 | [[package]] | ||
| 415 | name = "event-listener" | ||
| 416 | version = "5.4.1" | ||
| 417 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 418 | checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" | ||
| 419 | dependencies = [ | ||
| 420 | "concurrent-queue", | ||
| 421 | "parking", | ||
| 422 | "pin-project-lite", | ||
| 423 | ] | ||
| 424 | |||
| 425 | [[package]] | ||
| 191 | name = "fabrica" | 426 | name = "fabrica" |
| 192 | version = "0.1.0" | 427 | version = "0.1.0" |
| 193 | dependencies = [ | 428 | dependencies = [ |
| @@ -217,6 +452,29 @@ dependencies = [ | |||
| 217 | ] | 452 | ] |
| 218 | 453 | ||
| 219 | [[package]] | 454 | [[package]] |
| 455 | name = "find-msvc-tools" | ||
| 456 | version = "0.1.9" | ||
| 457 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 458 | checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | ||
| 459 | |||
| 460 | [[package]] | ||
| 461 | name = "flume" | ||
| 462 | version = "0.12.0" | ||
| 463 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 464 | checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" | ||
| 465 | dependencies = [ | ||
| 466 | "futures-core", | ||
| 467 | "futures-sink", | ||
| 468 | "spin", | ||
| 469 | ] | ||
| 470 | |||
| 471 | [[package]] | ||
| 472 | name = "foldhash" | ||
| 473 | version = "0.2.0" | ||
| 474 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 475 | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | ||
| 476 | |||
| 477 | [[package]] | ||
| 220 | name = "form_urlencoded" | 478 | name = "form_urlencoded" |
| 221 | version = "1.2.2" | 479 | version = "1.2.2" |
| 222 | source = "registry+https://github.com/rust-lang/crates.io-index" | 480 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -226,6 +484,99 @@ dependencies = [ | |||
| 226 | ] | 484 | ] |
| 227 | 485 | ||
| 228 | [[package]] | 486 | [[package]] |
| 487 | name = "futures-channel" | ||
| 488 | version = "0.3.33" | ||
| 489 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 490 | checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" | ||
| 491 | dependencies = [ | ||
| 492 | "futures-core", | ||
| 493 | "futures-sink", | ||
| 494 | ] | ||
| 495 | |||
| 496 | [[package]] | ||
| 497 | name = "futures-core" | ||
| 498 | version = "0.3.33" | ||
| 499 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 500 | checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" | ||
| 501 | |||
| 502 | [[package]] | ||
| 503 | name = "futures-executor" | ||
| 504 | version = "0.3.33" | ||
| 505 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 506 | checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" | ||
| 507 | dependencies = [ | ||
| 508 | "futures-core", | ||
| 509 | "futures-task", | ||
| 510 | "futures-util", | ||
| 511 | ] | ||
| 512 | |||
| 513 | [[package]] | ||
| 514 | name = "futures-intrusive" | ||
| 515 | version = "0.5.0" | ||
| 516 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 517 | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" | ||
| 518 | dependencies = [ | ||
| 519 | "futures-core", | ||
| 520 | "lock_api", | ||
| 521 | "parking_lot", | ||
| 522 | ] | ||
| 523 | |||
| 524 | [[package]] | ||
| 525 | name = "futures-io" | ||
| 526 | version = "0.3.33" | ||
| 527 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 528 | checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" | ||
| 529 | |||
| 530 | [[package]] | ||
| 531 | name = "futures-sink" | ||
| 532 | version = "0.3.33" | ||
| 533 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 534 | checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" | ||
| 535 | |||
| 536 | [[package]] | ||
| 537 | name = "futures-task" | ||
| 538 | version = "0.3.33" | ||
| 539 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 540 | checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" | ||
| 541 | |||
| 542 | [[package]] | ||
| 543 | name = "futures-util" | ||
| 544 | version = "0.3.33" | ||
| 545 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 546 | checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" | ||
| 547 | dependencies = [ | ||
| 548 | "futures-core", | ||
| 549 | "futures-io", | ||
| 550 | "futures-sink", | ||
| 551 | "futures-task", | ||
| 552 | "memchr", | ||
| 553 | "pin-project-lite", | ||
| 554 | "slab", | ||
| 555 | ] | ||
| 556 | |||
| 557 | [[package]] | ||
| 558 | name = "generic-array" | ||
| 559 | version = "0.14.9" | ||
| 560 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 561 | checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" | ||
| 562 | dependencies = [ | ||
| 563 | "typenum", | ||
| 564 | "version_check", | ||
| 565 | ] | ||
| 566 | |||
| 567 | [[package]] | ||
| 568 | name = "getrandom" | ||
| 569 | version = "0.3.4" | ||
| 570 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 571 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | ||
| 572 | dependencies = [ | ||
| 573 | "cfg-if", | ||
| 574 | "libc", | ||
| 575 | "r-efi 5.3.0", | ||
| 576 | "wasip2", | ||
| 577 | ] | ||
| 578 | |||
| 579 | [[package]] | ||
| 229 | name = "getrandom" | 580 | name = "getrandom" |
| 230 | version = "0.4.3" | 581 | version = "0.4.3" |
| 231 | source = "registry+https://github.com/rust-lang/crates.io-index" | 582 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -233,7 +584,8 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | |||
| 233 | dependencies = [ | 584 | dependencies = [ |
| 234 | "cfg-if", | 585 | "cfg-if", |
| 235 | "libc", | 586 | "libc", |
| 236 | "r-efi", | 587 | "r-efi 6.0.0", |
| 588 | "rand_core 0.10.1", | ||
| 237 | ] | 589 | ] |
| 238 | 590 | ||
| 239 | [[package]] | 591 | [[package]] |
| @@ -242,21 +594,74 @@ version = "0.1.0" | |||
| 242 | 594 | ||
| 243 | [[package]] | 595 | [[package]] |
| 244 | name = "hashbrown" | 596 | name = "hashbrown" |
| 597 | version = "0.16.1" | ||
| 598 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 599 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | ||
| 600 | dependencies = [ | ||
| 601 | "allocator-api2", | ||
| 602 | "equivalent", | ||
| 603 | "foldhash", | ||
| 604 | ] | ||
| 605 | |||
| 606 | [[package]] | ||
| 607 | name = "hashbrown" | ||
| 245 | version = "0.17.1" | 608 | version = "0.17.1" |
| 246 | source = "registry+https://github.com/rust-lang/crates.io-index" | 609 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 247 | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | 610 | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" |
| 248 | 611 | ||
| 249 | [[package]] | 612 | [[package]] |
| 613 | name = "hashlink" | ||
| 614 | version = "0.11.1" | ||
| 615 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 616 | checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" | ||
| 617 | dependencies = [ | ||
| 618 | "hashbrown 0.16.1", | ||
| 619 | ] | ||
| 620 | |||
| 621 | [[package]] | ||
| 250 | name = "heck" | 622 | name = "heck" |
| 251 | version = "0.5.0" | 623 | version = "0.5.0" |
| 252 | source = "registry+https://github.com/rust-lang/crates.io-index" | 624 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 253 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | 625 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" |
| 254 | 626 | ||
| 255 | [[package]] | 627 | [[package]] |
| 628 | name = "hex" | ||
| 629 | version = "0.4.3" | ||
| 630 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 631 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | ||
| 632 | |||
| 633 | [[package]] | ||
| 256 | name = "highlight" | 634 | name = "highlight" |
| 257 | version = "0.1.0" | 635 | version = "0.1.0" |
| 258 | 636 | ||
| 259 | [[package]] | 637 | [[package]] |
| 638 | name = "hkdf" | ||
| 639 | version = "0.13.0" | ||
| 640 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 641 | checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" | ||
| 642 | dependencies = [ | ||
| 643 | "hmac", | ||
| 644 | ] | ||
| 645 | |||
| 646 | [[package]] | ||
| 647 | name = "hmac" | ||
| 648 | version = "0.13.0" | ||
| 649 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 650 | checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" | ||
| 651 | dependencies = [ | ||
| 652 | "digest 0.11.3", | ||
| 653 | ] | ||
| 654 | |||
| 655 | [[package]] | ||
| 656 | name = "hybrid-array" | ||
| 657 | version = "0.4.13" | ||
| 658 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 659 | checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" | ||
| 660 | dependencies = [ | ||
| 661 | "typenum", | ||
| 662 | ] | ||
| 663 | |||
| 664 | [[package]] | ||
| 260 | name = "icu_collections" | 665 | name = "icu_collections" |
| 261 | version = "2.1.1" | 666 | version = "2.1.1" |
| 262 | source = "registry+https://github.com/rust-lang/crates.io-index" | 667 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -365,7 +770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 365 | checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" | 770 | checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" |
| 366 | dependencies = [ | 771 | dependencies = [ |
| 367 | "equivalent", | 772 | "equivalent", |
| 368 | "hashbrown", | 773 | "hashbrown 0.17.1", |
| 369 | ] | 774 | ] |
| 370 | 775 | ||
| 371 | [[package]] | 776 | [[package]] |
| @@ -387,12 +792,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 387 | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" | 792 | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" |
| 388 | 793 | ||
| 389 | [[package]] | 794 | [[package]] |
| 795 | name = "js-sys" | ||
| 796 | version = "0.3.103" | ||
| 797 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 798 | checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" | ||
| 799 | dependencies = [ | ||
| 800 | "cfg-if", | ||
| 801 | "futures-util", | ||
| 802 | "wasm-bindgen", | ||
| 803 | ] | ||
| 804 | |||
| 805 | [[package]] | ||
| 390 | name = "libc" | 806 | name = "libc" |
| 391 | version = "0.2.189" | 807 | version = "0.2.189" |
| 392 | source = "registry+https://github.com/rust-lang/crates.io-index" | 808 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 393 | checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" | 809 | checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" |
| 394 | 810 | ||
| 395 | [[package]] | 811 | [[package]] |
| 812 | name = "libsqlite3-sys" | ||
| 813 | version = "0.37.0" | ||
| 814 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 815 | checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" | ||
| 816 | dependencies = [ | ||
| 817 | "cc", | ||
| 818 | "pkg-config", | ||
| 819 | "vcpkg", | ||
| 820 | ] | ||
| 821 | |||
| 822 | [[package]] | ||
| 396 | name = "linux-raw-sys" | 823 | name = "linux-raw-sys" |
| 397 | version = "0.12.1" | 824 | version = "0.12.1" |
| 398 | source = "registry+https://github.com/rust-lang/crates.io-index" | 825 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -414,18 +841,57 @@ dependencies = [ | |||
| 414 | ] | 841 | ] |
| 415 | 842 | ||
| 416 | [[package]] | 843 | [[package]] |
| 844 | name = "log" | ||
| 845 | version = "0.4.33" | ||
| 846 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 847 | checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" | ||
| 848 | |||
| 849 | [[package]] | ||
| 417 | name = "mail" | 850 | name = "mail" |
| 418 | version = "0.1.0" | 851 | version = "0.1.0" |
| 419 | 852 | ||
| 420 | [[package]] | 853 | [[package]] |
| 854 | name = "md-5" | ||
| 855 | version = "0.11.0" | ||
| 856 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 857 | checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" | ||
| 858 | dependencies = [ | ||
| 859 | "cfg-if", | ||
| 860 | "digest 0.11.3", | ||
| 861 | ] | ||
| 862 | |||
| 863 | [[package]] | ||
| 421 | name = "memchr" | 864 | name = "memchr" |
| 422 | version = "2.8.3" | 865 | version = "2.8.3" |
| 423 | source = "registry+https://github.com/rust-lang/crates.io-index" | 866 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 424 | checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" | 867 | checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" |
| 425 | 868 | ||
| 426 | [[package]] | 869 | [[package]] |
| 427 | name = "model" | 870 | name = "mio" |
| 428 | version = "0.1.0" | 871 | version = "1.2.2" |
| 872 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 873 | checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" | ||
| 874 | dependencies = [ | ||
| 875 | "libc", | ||
| 876 | "wasi", | ||
| 877 | "windows-sys", | ||
| 878 | ] | ||
| 879 | |||
| 880 | [[package]] | ||
| 881 | name = "model" | ||
| 882 | version = "0.1.0" | ||
| 883 | dependencies = [ | ||
| 884 | "thiserror", | ||
| 885 | ] | ||
| 886 | |||
| 887 | [[package]] | ||
| 888 | name = "num-traits" | ||
| 889 | version = "0.2.19" | ||
| 890 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 891 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | ||
| 892 | dependencies = [ | ||
| 893 | "autocfg", | ||
| 894 | ] | ||
| 429 | 895 | ||
| 430 | [[package]] | 896 | [[package]] |
| 431 | name = "once_cell" | 897 | name = "once_cell" |
| @@ -440,6 +906,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 440 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | 906 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" |
| 441 | 907 | ||
| 442 | [[package]] | 908 | [[package]] |
| 909 | name = "parking" | ||
| 910 | version = "2.2.1" | ||
| 911 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 912 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" | ||
| 913 | |||
| 914 | [[package]] | ||
| 443 | name = "parking_lot" | 915 | name = "parking_lot" |
| 444 | version = "0.12.5" | 916 | version = "0.12.5" |
| 445 | source = "registry+https://github.com/rust-lang/crates.io-index" | 917 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -492,6 +964,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 492 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | 964 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" |
| 493 | 965 | ||
| 494 | [[package]] | 966 | [[package]] |
| 967 | name = "pin-project-lite" | ||
| 968 | version = "0.2.17" | ||
| 969 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 970 | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | ||
| 971 | |||
| 972 | [[package]] | ||
| 973 | name = "pkg-config" | ||
| 974 | version = "0.3.33" | ||
| 975 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 976 | checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" | ||
| 977 | |||
| 978 | [[package]] | ||
| 495 | name = "potential_utf" | 979 | name = "potential_utf" |
| 496 | version = "0.1.5" | 980 | version = "0.1.5" |
| 497 | source = "registry+https://github.com/rust-lang/crates.io-index" | 981 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -501,6 +985,15 @@ dependencies = [ | |||
| 501 | ] | 985 | ] |
| 502 | 986 | ||
| 503 | [[package]] | 987 | [[package]] |
| 988 | name = "ppv-lite86" | ||
| 989 | version = "0.2.21" | ||
| 990 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 991 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||
| 992 | dependencies = [ | ||
| 993 | "zerocopy", | ||
| 994 | ] | ||
| 995 | |||
| 996 | [[package]] | ||
| 504 | name = "proc-macro2" | 997 | name = "proc-macro2" |
| 505 | version = "1.0.107" | 998 | version = "1.0.107" |
| 506 | source = "registry+https://github.com/rust-lang/crates.io-index" | 999 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -533,11 +1026,63 @@ dependencies = [ | |||
| 533 | 1026 | ||
| 534 | [[package]] | 1027 | [[package]] |
| 535 | name = "r-efi" | 1028 | name = "r-efi" |
| 1029 | version = "5.3.0" | ||
| 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1031 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | ||
| 1032 | |||
| 1033 | [[package]] | ||
| 1034 | name = "r-efi" | ||
| 536 | version = "6.0.0" | 1035 | version = "6.0.0" |
| 537 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 538 | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | 1037 | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" |
| 539 | 1038 | ||
| 540 | [[package]] | 1039 | [[package]] |
| 1040 | name = "rand" | ||
| 1041 | version = "0.9.5" | ||
| 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1043 | checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" | ||
| 1044 | dependencies = [ | ||
| 1045 | "rand_chacha", | ||
| 1046 | "rand_core 0.9.5", | ||
| 1047 | ] | ||
| 1048 | |||
| 1049 | [[package]] | ||
| 1050 | name = "rand" | ||
| 1051 | version = "0.10.2" | ||
| 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1053 | checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" | ||
| 1054 | dependencies = [ | ||
| 1055 | "chacha20", | ||
| 1056 | "getrandom 0.4.3", | ||
| 1057 | "rand_core 0.10.1", | ||
| 1058 | ] | ||
| 1059 | |||
| 1060 | [[package]] | ||
| 1061 | name = "rand_chacha" | ||
| 1062 | version = "0.9.0" | ||
| 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1064 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | ||
| 1065 | dependencies = [ | ||
| 1066 | "ppv-lite86", | ||
| 1067 | "rand_core 0.9.5", | ||
| 1068 | ] | ||
| 1069 | |||
| 1070 | [[package]] | ||
| 1071 | name = "rand_core" | ||
| 1072 | version = "0.9.5" | ||
| 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1074 | checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" | ||
| 1075 | dependencies = [ | ||
| 1076 | "getrandom 0.3.4", | ||
| 1077 | ] | ||
| 1078 | |||
| 1079 | [[package]] | ||
| 1080 | name = "rand_core" | ||
| 1081 | version = "0.10.1" | ||
| 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1083 | checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" | ||
| 1084 | |||
| 1085 | [[package]] | ||
| 541 | name = "redox_syscall" | 1086 | name = "redox_syscall" |
| 542 | version = "0.5.18" | 1087 | version = "0.5.18" |
| 543 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -560,6 +1105,12 @@ dependencies = [ | |||
| 560 | ] | 1105 | ] |
| 561 | 1106 | ||
| 562 | [[package]] | 1107 | [[package]] |
| 1108 | name = "rustversion" | ||
| 1109 | version = "1.0.23" | ||
| 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1111 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | ||
| 1112 | |||
| 1113 | [[package]] | ||
| 563 | name = "scopeguard" | 1114 | name = "scopeguard" |
| 564 | version = "1.2.0" | 1115 | version = "1.2.0" |
| 565 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -618,10 +1169,247 @@ dependencies = [ | |||
| 618 | ] | 1169 | ] |
| 619 | 1170 | ||
| 620 | [[package]] | 1171 | [[package]] |
| 1172 | name = "sha1" | ||
| 1173 | version = "0.11.0" | ||
| 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1175 | checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" | ||
| 1176 | dependencies = [ | ||
| 1177 | "cfg-if", | ||
| 1178 | "cpufeatures 0.3.0", | ||
| 1179 | "digest 0.11.3", | ||
| 1180 | ] | ||
| 1181 | |||
| 1182 | [[package]] | ||
| 1183 | name = "sha2" | ||
| 1184 | version = "0.10.9" | ||
| 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1186 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | ||
| 1187 | dependencies = [ | ||
| 1188 | "cfg-if", | ||
| 1189 | "cpufeatures 0.2.17", | ||
| 1190 | "digest 0.10.7", | ||
| 1191 | ] | ||
| 1192 | |||
| 1193 | [[package]] | ||
| 1194 | name = "sha2" | ||
| 1195 | version = "0.11.0" | ||
| 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1197 | checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" | ||
| 1198 | dependencies = [ | ||
| 1199 | "cfg-if", | ||
| 1200 | "cpufeatures 0.3.0", | ||
| 1201 | "digest 0.11.3", | ||
| 1202 | ] | ||
| 1203 | |||
| 1204 | [[package]] | ||
| 1205 | name = "shlex" | ||
| 1206 | version = "2.0.1" | ||
| 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1208 | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | ||
| 1209 | |||
| 1210 | [[package]] | ||
| 1211 | name = "slab" | ||
| 1212 | version = "0.4.12" | ||
| 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1214 | checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | ||
| 1215 | |||
| 1216 | [[package]] | ||
| 621 | name = "smallvec" | 1217 | name = "smallvec" |
| 622 | version = "1.15.2" | 1218 | version = "1.15.2" |
| 623 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 624 | checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" | 1220 | checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" |
| 1221 | dependencies = [ | ||
| 1222 | "serde", | ||
| 1223 | ] | ||
| 1224 | |||
| 1225 | [[package]] | ||
| 1226 | name = "socket2" | ||
| 1227 | version = "0.6.5" | ||
| 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1229 | checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" | ||
| 1230 | dependencies = [ | ||
| 1231 | "libc", | ||
| 1232 | "windows-sys", | ||
| 1233 | ] | ||
| 1234 | |||
| 1235 | [[package]] | ||
| 1236 | name = "spin" | ||
| 1237 | version = "0.9.9" | ||
| 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1239 | checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" | ||
| 1240 | dependencies = [ | ||
| 1241 | "lock_api", | ||
| 1242 | ] | ||
| 1243 | |||
| 1244 | [[package]] | ||
| 1245 | name = "sqlx" | ||
| 1246 | version = "0.9.0" | ||
| 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1248 | checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" | ||
| 1249 | dependencies = [ | ||
| 1250 | "sqlx-core", | ||
| 1251 | "sqlx-macros", | ||
| 1252 | "sqlx-mysql", | ||
| 1253 | "sqlx-postgres", | ||
| 1254 | "sqlx-sqlite", | ||
| 1255 | ] | ||
| 1256 | |||
| 1257 | [[package]] | ||
| 1258 | name = "sqlx-core" | ||
| 1259 | version = "0.9.0" | ||
| 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1261 | checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" | ||
| 1262 | dependencies = [ | ||
| 1263 | "base64", | ||
| 1264 | "bytes", | ||
| 1265 | "cfg-if", | ||
| 1266 | "crc", | ||
| 1267 | "crossbeam-queue", | ||
| 1268 | "either", | ||
| 1269 | "event-listener", | ||
| 1270 | "futures-core", | ||
| 1271 | "futures-intrusive", | ||
| 1272 | "futures-io", | ||
| 1273 | "futures-util", | ||
| 1274 | "hashbrown 0.16.1", | ||
| 1275 | "hashlink", | ||
| 1276 | "indexmap", | ||
| 1277 | "log", | ||
| 1278 | "memchr", | ||
| 1279 | "percent-encoding", | ||
| 1280 | "serde", | ||
| 1281 | "serde_json", | ||
| 1282 | "sha2 0.10.9", | ||
| 1283 | "smallvec", | ||
| 1284 | "thiserror", | ||
| 1285 | "tokio", | ||
| 1286 | "tokio-stream", | ||
| 1287 | "tracing", | ||
| 1288 | "url", | ||
| 1289 | ] | ||
| 1290 | |||
| 1291 | [[package]] | ||
| 1292 | name = "sqlx-macros" | ||
| 1293 | version = "0.9.0" | ||
| 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1295 | checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" | ||
| 1296 | dependencies = [ | ||
| 1297 | "proc-macro2", | ||
| 1298 | "quote", | ||
| 1299 | "sqlx-core", | ||
| 1300 | "sqlx-macros-core", | ||
| 1301 | "syn 2.0.119", | ||
| 1302 | ] | ||
| 1303 | |||
| 1304 | [[package]] | ||
| 1305 | name = "sqlx-macros-core" | ||
| 1306 | version = "0.9.0" | ||
| 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1308 | checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" | ||
| 1309 | dependencies = [ | ||
| 1310 | "cfg-if", | ||
| 1311 | "dotenvy", | ||
| 1312 | "either", | ||
| 1313 | "heck", | ||
| 1314 | "hex", | ||
| 1315 | "proc-macro2", | ||
| 1316 | "quote", | ||
| 1317 | "serde", | ||
| 1318 | "serde_json", | ||
| 1319 | "sha2 0.10.9", | ||
| 1320 | "sqlx-core", | ||
| 1321 | "sqlx-postgres", | ||
| 1322 | "sqlx-sqlite", | ||
| 1323 | "syn 2.0.119", | ||
| 1324 | "thiserror", | ||
| 1325 | "tokio", | ||
| 1326 | "url", | ||
| 1327 | ] | ||
| 1328 | |||
| 1329 | [[package]] | ||
| 1330 | name = "sqlx-mysql" | ||
| 1331 | version = "0.9.0" | ||
| 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1333 | checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" | ||
| 1334 | dependencies = [ | ||
| 1335 | "bitflags", | ||
| 1336 | "byteorder", | ||
| 1337 | "bytes", | ||
| 1338 | "crc", | ||
| 1339 | "digest 0.11.3", | ||
| 1340 | "dotenvy", | ||
| 1341 | "either", | ||
| 1342 | "futures-core", | ||
| 1343 | "futures-util", | ||
| 1344 | "generic-array", | ||
| 1345 | "log", | ||
| 1346 | "percent-encoding", | ||
| 1347 | "serde", | ||
| 1348 | "sha1", | ||
| 1349 | "sha2 0.11.0", | ||
| 1350 | "sqlx-core", | ||
| 1351 | "thiserror", | ||
| 1352 | "tracing", | ||
| 1353 | ] | ||
| 1354 | |||
| 1355 | [[package]] | ||
| 1356 | name = "sqlx-postgres" | ||
| 1357 | version = "0.9.0" | ||
| 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1359 | checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" | ||
| 1360 | dependencies = [ | ||
| 1361 | "atoi", | ||
| 1362 | "base64", | ||
| 1363 | "bitflags", | ||
| 1364 | "byteorder", | ||
| 1365 | "crc", | ||
| 1366 | "dotenvy", | ||
| 1367 | "etcetera", | ||
| 1368 | "futures-channel", | ||
| 1369 | "futures-core", | ||
| 1370 | "futures-util", | ||
| 1371 | "hex", | ||
| 1372 | "hkdf", | ||
| 1373 | "hmac", | ||
| 1374 | "itoa", | ||
| 1375 | "log", | ||
| 1376 | "md-5", | ||
| 1377 | "memchr", | ||
| 1378 | "rand 0.10.2", | ||
| 1379 | "serde", | ||
| 1380 | "serde_json", | ||
| 1381 | "sha2 0.11.0", | ||
| 1382 | "smallvec", | ||
| 1383 | "sqlx-core", | ||
| 1384 | "stringprep", | ||
| 1385 | "thiserror", | ||
| 1386 | "tracing", | ||
| 1387 | "whoami", | ||
| 1388 | ] | ||
| 1389 | |||
| 1390 | [[package]] | ||
| 1391 | name = "sqlx-sqlite" | ||
| 1392 | version = "0.9.0" | ||
| 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1394 | checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" | ||
| 1395 | dependencies = [ | ||
| 1396 | "atoi", | ||
| 1397 | "flume", | ||
| 1398 | "form_urlencoded", | ||
| 1399 | "futures-channel", | ||
| 1400 | "futures-core", | ||
| 1401 | "futures-executor", | ||
| 1402 | "futures-intrusive", | ||
| 1403 | "futures-util", | ||
| 1404 | "libsqlite3-sys", | ||
| 1405 | "log", | ||
| 1406 | "percent-encoding", | ||
| 1407 | "serde", | ||
| 1408 | "sqlx-core", | ||
| 1409 | "thiserror", | ||
| 1410 | "tracing", | ||
| 1411 | "url", | ||
| 1412 | ] | ||
| 625 | 1413 | ||
| 626 | [[package]] | 1414 | [[package]] |
| 627 | name = "ssh" | 1415 | name = "ssh" |
| @@ -636,6 +1424,25 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" | |||
| 636 | [[package]] | 1424 | [[package]] |
| 637 | name = "store" | 1425 | name = "store" |
| 638 | version = "0.1.0" | 1426 | version = "0.1.0" |
| 1427 | dependencies = [ | ||
| 1428 | "model", | ||
| 1429 | "sqlx", | ||
| 1430 | "tempfile", | ||
| 1431 | "thiserror", | ||
| 1432 | "tokio", | ||
| 1433 | "ulid", | ||
| 1434 | ] | ||
| 1435 | |||
| 1436 | [[package]] | ||
| 1437 | name = "stringprep" | ||
| 1438 | version = "0.1.5" | ||
| 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1440 | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" | ||
| 1441 | dependencies = [ | ||
| 1442 | "unicode-bidi", | ||
| 1443 | "unicode-normalization", | ||
| 1444 | "unicode-properties", | ||
| 1445 | ] | ||
| 639 | 1446 | ||
| 640 | [[package]] | 1447 | [[package]] |
| 641 | name = "strsim" | 1448 | name = "strsim" |
| @@ -683,7 +1490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 683 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | 1490 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" |
| 684 | dependencies = [ | 1491 | dependencies = [ |
| 685 | "fastrand", | 1492 | "fastrand", |
| 686 | "getrandom", | 1493 | "getrandom 0.4.3", |
| 687 | "once_cell", | 1494 | "once_cell", |
| 688 | "rustix", | 1495 | "rustix", |
| 689 | "windows-sys", | 1496 | "windows-sys", |
| @@ -720,6 +1527,58 @@ dependencies = [ | |||
| 720 | ] | 1527 | ] |
| 721 | 1528 | ||
| 722 | [[package]] | 1529 | [[package]] |
| 1530 | name = "tinyvec" | ||
| 1531 | version = "1.12.0" | ||
| 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1533 | checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" | ||
| 1534 | dependencies = [ | ||
| 1535 | "tinyvec_macros", | ||
| 1536 | ] | ||
| 1537 | |||
| 1538 | [[package]] | ||
| 1539 | name = "tinyvec_macros" | ||
| 1540 | version = "0.1.1" | ||
| 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1542 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" | ||
| 1543 | |||
| 1544 | [[package]] | ||
| 1545 | name = "tokio" | ||
| 1546 | version = "1.53.1" | ||
| 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1548 | checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" | ||
| 1549 | dependencies = [ | ||
| 1550 | "bytes", | ||
| 1551 | "libc", | ||
| 1552 | "mio", | ||
| 1553 | "pin-project-lite", | ||
| 1554 | "socket2", | ||
| 1555 | "tokio-macros", | ||
| 1556 | "windows-sys", | ||
| 1557 | ] | ||
| 1558 | |||
| 1559 | [[package]] | ||
| 1560 | name = "tokio-macros" | ||
| 1561 | version = "2.7.1" | ||
| 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1563 | checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" | ||
| 1564 | dependencies = [ | ||
| 1565 | "proc-macro2", | ||
| 1566 | "quote", | ||
| 1567 | "syn 2.0.119", | ||
| 1568 | ] | ||
| 1569 | |||
| 1570 | [[package]] | ||
| 1571 | name = "tokio-stream" | ||
| 1572 | version = "0.1.19" | ||
| 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1574 | checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" | ||
| 1575 | dependencies = [ | ||
| 1576 | "futures-core", | ||
| 1577 | "pin-project-lite", | ||
| 1578 | "tokio", | ||
| 1579 | ] | ||
| 1580 | |||
| 1581 | [[package]] | ||
| 723 | name = "toml" | 1582 | name = "toml" |
| 724 | version = "0.8.23" | 1583 | version = "0.8.23" |
| 725 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -761,6 +1620,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 761 | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" | 1620 | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" |
| 762 | 1621 | ||
| 763 | [[package]] | 1622 | [[package]] |
| 1623 | name = "tracing" | ||
| 1624 | version = "0.1.44" | ||
| 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1626 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | ||
| 1627 | dependencies = [ | ||
| 1628 | "log", | ||
| 1629 | "pin-project-lite", | ||
| 1630 | "tracing-attributes", | ||
| 1631 | "tracing-core", | ||
| 1632 | ] | ||
| 1633 | |||
| 1634 | [[package]] | ||
| 1635 | name = "tracing-attributes" | ||
| 1636 | version = "0.1.31" | ||
| 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1638 | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | ||
| 1639 | dependencies = [ | ||
| 1640 | "proc-macro2", | ||
| 1641 | "quote", | ||
| 1642 | "syn 2.0.119", | ||
| 1643 | ] | ||
| 1644 | |||
| 1645 | [[package]] | ||
| 1646 | name = "tracing-core" | ||
| 1647 | version = "0.1.36" | ||
| 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1649 | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | ||
| 1650 | dependencies = [ | ||
| 1651 | "once_cell", | ||
| 1652 | ] | ||
| 1653 | |||
| 1654 | [[package]] | ||
| 1655 | name = "typenum" | ||
| 1656 | version = "1.20.1" | ||
| 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1658 | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | ||
| 1659 | |||
| 1660 | [[package]] | ||
| 1661 | name = "ulid" | ||
| 1662 | version = "1.2.1" | ||
| 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1664 | checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" | ||
| 1665 | dependencies = [ | ||
| 1666 | "rand 0.9.5", | ||
| 1667 | "web-time", | ||
| 1668 | ] | ||
| 1669 | |||
| 1670 | [[package]] | ||
| 764 | name = "uncased" | 1671 | name = "uncased" |
| 765 | version = "0.9.10" | 1672 | version = "0.9.10" |
| 766 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -770,12 +1677,33 @@ dependencies = [ | |||
| 770 | ] | 1677 | ] |
| 771 | 1678 | ||
| 772 | [[package]] | 1679 | [[package]] |
| 1680 | name = "unicode-bidi" | ||
| 1681 | version = "0.3.18" | ||
| 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1683 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" | ||
| 1684 | |||
| 1685 | [[package]] | ||
| 773 | name = "unicode-ident" | 1686 | name = "unicode-ident" |
| 774 | version = "1.0.24" | 1687 | version = "1.0.24" |
| 775 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 776 | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | 1689 | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" |
| 777 | 1690 | ||
| 778 | [[package]] | 1691 | [[package]] |
| 1692 | name = "unicode-normalization" | ||
| 1693 | version = "0.1.25" | ||
| 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1695 | checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" | ||
| 1696 | dependencies = [ | ||
| 1697 | "tinyvec", | ||
| 1698 | ] | ||
| 1699 | |||
| 1700 | [[package]] | ||
| 1701 | name = "unicode-properties" | ||
| 1702 | version = "0.1.4" | ||
| 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1704 | checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" | ||
| 1705 | |||
| 1706 | [[package]] | ||
| 779 | name = "url" | 1707 | name = "url" |
| 780 | version = "2.5.8" | 1708 | version = "2.5.8" |
| 781 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -800,16 +1728,98 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 800 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | 1728 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" |
| 801 | 1729 | ||
| 802 | [[package]] | 1730 | [[package]] |
| 1731 | name = "vcpkg" | ||
| 1732 | version = "0.2.15" | ||
| 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1734 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | ||
| 1735 | |||
| 1736 | [[package]] | ||
| 803 | name = "version_check" | 1737 | name = "version_check" |
| 804 | version = "0.9.5" | 1738 | version = "0.9.5" |
| 805 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1739 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 806 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | 1740 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" |
| 807 | 1741 | ||
| 808 | [[package]] | 1742 | [[package]] |
| 1743 | name = "wasi" | ||
| 1744 | version = "0.11.1+wasi-snapshot-preview1" | ||
| 1745 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1746 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | ||
| 1747 | |||
| 1748 | [[package]] | ||
| 1749 | name = "wasip2" | ||
| 1750 | version = "1.0.1+wasi-0.2.4" | ||
| 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1752 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" | ||
| 1753 | dependencies = [ | ||
| 1754 | "wit-bindgen", | ||
| 1755 | ] | ||
| 1756 | |||
| 1757 | [[package]] | ||
| 1758 | name = "wasm-bindgen" | ||
| 1759 | version = "0.2.126" | ||
| 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1761 | checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" | ||
| 1762 | dependencies = [ | ||
| 1763 | "cfg-if", | ||
| 1764 | "once_cell", | ||
| 1765 | "rustversion", | ||
| 1766 | "wasm-bindgen-macro", | ||
| 1767 | "wasm-bindgen-shared", | ||
| 1768 | ] | ||
| 1769 | |||
| 1770 | [[package]] | ||
| 1771 | name = "wasm-bindgen-macro" | ||
| 1772 | version = "0.2.126" | ||
| 1773 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1774 | checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" | ||
| 1775 | dependencies = [ | ||
| 1776 | "quote", | ||
| 1777 | "wasm-bindgen-macro-support", | ||
| 1778 | ] | ||
| 1779 | |||
| 1780 | [[package]] | ||
| 1781 | name = "wasm-bindgen-macro-support" | ||
| 1782 | version = "0.2.126" | ||
| 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1784 | checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" | ||
| 1785 | dependencies = [ | ||
| 1786 | "bumpalo", | ||
| 1787 | "proc-macro2", | ||
| 1788 | "quote", | ||
| 1789 | "syn 2.0.119", | ||
| 1790 | "wasm-bindgen-shared", | ||
| 1791 | ] | ||
| 1792 | |||
| 1793 | [[package]] | ||
| 1794 | name = "wasm-bindgen-shared" | ||
| 1795 | version = "0.2.126" | ||
| 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1797 | checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" | ||
| 1798 | dependencies = [ | ||
| 1799 | "unicode-ident", | ||
| 1800 | ] | ||
| 1801 | |||
| 1802 | [[package]] | ||
| 809 | name = "web" | 1803 | name = "web" |
| 810 | version = "0.1.0" | 1804 | version = "0.1.0" |
| 811 | 1805 | ||
| 812 | [[package]] | 1806 | [[package]] |
| 1807 | name = "web-time" | ||
| 1808 | version = "1.1.0" | ||
| 1809 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1810 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | ||
| 1811 | dependencies = [ | ||
| 1812 | "js-sys", | ||
| 1813 | "wasm-bindgen", | ||
| 1814 | ] | ||
| 1815 | |||
| 1816 | [[package]] | ||
| 1817 | name = "whoami" | ||
| 1818 | version = "2.1.2" | ||
| 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1820 | checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" | ||
| 1821 | |||
| 1822 | [[package]] | ||
| 813 | name = "windows-link" | 1823 | name = "windows-link" |
| 814 | version = "0.2.1" | 1824 | version = "0.2.1" |
| 815 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -834,6 +1844,12 @@ dependencies = [ | |||
| 834 | ] | 1844 | ] |
| 835 | 1845 | ||
| 836 | [[package]] | 1846 | [[package]] |
| 1847 | name = "wit-bindgen" | ||
| 1848 | version = "0.46.0" | ||
| 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1850 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" | ||
| 1851 | |||
| 1852 | [[package]] | ||
| 837 | name = "writeable" | 1853 | name = "writeable" |
| 838 | version = "0.6.3" | 1854 | version = "0.6.3" |
| 839 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -869,6 +1885,26 @@ dependencies = [ | |||
| 869 | ] | 1885 | ] |
| 870 | 1886 | ||
| 871 | [[package]] | 1887 | [[package]] |
| 1888 | name = "zerocopy" | ||
| 1889 | version = "0.8.55" | ||
| 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1891 | checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" | ||
| 1892 | dependencies = [ | ||
| 1893 | "zerocopy-derive", | ||
| 1894 | ] | ||
| 1895 | |||
| 1896 | [[package]] | ||
| 1897 | name = "zerocopy-derive" | ||
| 1898 | version = "0.8.55" | ||
| 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1900 | checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" | ||
| 1901 | dependencies = [ | ||
| 1902 | "proc-macro2", | ||
| 1903 | "quote", | ||
| 1904 | "syn 2.0.119", | ||
| 1905 | ] | ||
| 1906 | |||
| 1907 | [[package]] | ||
| 872 | name = "zerofrom" | 1908 | name = "zerofrom" |
| 873 | version = "0.1.8" | 1909 | version = "0.1.8" |
| 874 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1910 | source = "registry+https://github.com/rust-lang/crates.io-index" |
Cargo.toml +14 −0
| @@ -58,6 +58,20 @@ thiserror = "2" | |||
| 58 | url = "2" | 58 | url = "2" |
| 59 | clap = { version = "4", features = ["derive"] } | 59 | clap = { version = "4", features = ["derive"] } |
| 60 | anyhow = "1" | 60 | anyhow = "1" |
| 61 | # Database. `any` gives one code path over both backends; TLS is deliberately | ||
| 62 | # omitted for now (Postgres-over-TLS is a phase-17 deployment concern) to keep | ||
| 63 | # the dependency tree small and the license set clean — see docs/decisions.md. | ||
| 64 | sqlx = { version = "0.9", default-features = false, features = [ | ||
| 65 | "runtime-tokio", | ||
| 66 | "any", | ||
| 67 | "sqlite", | ||
| 68 | "postgres", | ||
| 69 | "migrate", | ||
| 70 | "macros", | ||
| 71 | ] } | ||
| 72 | ulid = "1" | ||
| 73 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } | ||
| 74 | tempfile = "3" | ||
| 61 | 75 | ||
| 62 | [workspace.lints.rust] | 76 | [workspace.lints.rust] |
| 63 | unsafe_code = "forbid" | 77 | unsafe_code = "forbid" |
crates/model/Cargo.toml +1 −0
| @@ -10,6 +10,7 @@ repository.workspace = true | |||
| 10 | publish.workspace = true | 10 | publish.workspace = true |
| 11 | 11 | ||
| 12 | [dependencies] | 12 | [dependencies] |
| 13 | thiserror = { workspace = true } | ||
| 13 | 14 | ||
| 14 | [lints] | 15 | [lints] |
| 15 | workspace = true | 16 | workspace = true |
crates/model/src/entity.rs +75 −0
| @@ -0,0 +1,75 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! In-memory representations of the persisted entities. | ||
| 6 | //! | ||
| 7 | //! These are plain data types with no behaviour and no I/O — [`crate::store`] | ||
| 8 | //! (the `store` crate) is responsible for reading and writing them. Timestamps | ||
| 9 | //! are Unix milliseconds UTC ([`Timestamp`]); identifiers are ULID strings. | ||
| 10 | //! | ||
| 11 | //! Storage-only columns (the normalized `*_lower` uniqueness keys) are | ||
| 12 | //! deliberately absent: they are an implementation detail of the schema, not | ||
| 13 | //! part of the domain. | ||
| 14 | |||
| 15 | /// A Unix timestamp in **milliseconds** UTC, matching the `BIGINT` columns in | ||
| 16 | /// the schema. | ||
| 17 | pub type Timestamp = i64; | ||
| 18 | |||
| 19 | /// A registered account. | ||
| 20 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 21 | pub struct User { | ||
| 22 | /// ULID primary key. | ||
| 23 | pub id: String, | ||
| 24 | /// The name as the user typed it (case preserved for display). | ||
| 25 | pub username: String, | ||
| 26 | /// The primary email address. | ||
| 27 | pub email: String, | ||
| 28 | /// Optional human-friendly display name. | ||
| 29 | pub display_name: Option<String>, | ||
| 30 | /// Argon2id PHC hash, or `None` until an invite is completed. | ||
| 31 | pub password_hash: Option<String>, | ||
| 32 | /// Whether the user must set a new password on next login. | ||
| 33 | pub must_change_password: bool, | ||
| 34 | /// Whether the user is a site administrator. | ||
| 35 | pub is_admin: bool, | ||
| 36 | /// When the account was disabled, if it is. | ||
| 37 | pub disabled_at: Option<Timestamp>, | ||
| 38 | /// Creation time. | ||
| 39 | pub created_at: Timestamp, | ||
| 40 | /// Last modification time. | ||
| 41 | pub updated_at: Timestamp, | ||
| 42 | } | ||
| 43 | |||
| 44 | /// A git repository. On disk it lives at `{repo_dir}/{id[0..2]}/{id}.git`; the | ||
| 45 | /// database is the sole authority mapping `(owner, path)` to this id. | ||
| 46 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 47 | pub struct Repo { | ||
| 48 | /// ULID primary key, and the on-disk directory stem. | ||
| 49 | pub id: String, | ||
| 50 | /// Owning user. | ||
| 51 | pub owner_id: String, | ||
| 52 | /// Containing group, or `None` when the repo hangs directly off the owner. | ||
| 53 | pub group_id: Option<String>, | ||
| 54 | /// The repo's own name (the final path segment). | ||
| 55 | pub name: String, | ||
| 56 | /// Materialized full path within the owner, e.g. `group/sub/repo`; equal to | ||
| 57 | /// `name` when ungrouped. | ||
| 58 | pub path: String, | ||
| 59 | /// Optional one-line description. | ||
| 60 | pub description: Option<String>, | ||
| 61 | /// Private repositories are invisible to non-collaborators. | ||
| 62 | pub is_private: bool, | ||
| 63 | /// The branch shown by default and used for HEAD. | ||
| 64 | pub default_branch: String, | ||
| 65 | /// When the repo was archived (read-only), if it is. | ||
| 66 | pub archived_at: Option<Timestamp>, | ||
| 67 | /// Approximate on-disk size in bytes, refreshed by maintenance. | ||
| 68 | pub size_bytes: i64, | ||
| 69 | /// Time of the last push, if any. | ||
| 70 | pub pushed_at: Option<Timestamp>, | ||
| 71 | /// Creation time. | ||
| 72 | pub created_at: Timestamp, | ||
| 73 | /// Last modification time. | ||
| 74 | pub updated_at: Timestamp, | ||
| 75 | } | ||
crates/model/src/lib.rs +10 −0
| @@ -3,3 +3,13 @@ | |||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | 4 | ||
| 5 | //! Shared domain types. This crate performs no I/O and depends on nothing else in-tree. | 5 | //! Shared domain types. This crate performs no I/O and depends on nothing else in-tree. |
| 6 | //! | ||
| 7 | //! It holds the in-memory shapes of persisted entities ([`entity`]) and the | ||
| 8 | //! name-validation rules ([`name`]) that every entry point — CLI, API, web — | ||
| 9 | //! shares so they cannot drift. | ||
| 10 | |||
| 11 | pub mod entity; | ||
| 12 | pub mod name; | ||
| 13 | |||
| 14 | pub use entity::{Repo, Timestamp, User}; | ||
| 15 | pub use name::{MAX_NAME_LEN, NameError, RESERVED_NAMES, validate_name}; | ||
crates/model/src/name.rs +201 −0
| @@ -0,0 +1,201 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! Validation for user, repository, and group names. | ||
| 6 | //! | ||
| 7 | //! One implementation, used by the CLI, the API, and the web UI, so the rules | ||
| 8 | //! can never drift between entry points. A valid name matches the regular | ||
| 9 | //! expression | ||
| 10 | //! | ||
| 11 | //! ```text | ||
| 12 | //! ^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$ | ||
| 13 | //! ``` | ||
| 14 | //! | ||
| 15 | //! and additionally must not be `.` or `..`, must not end in `.git` or `.`, and | ||
| 16 | //! must not collide with a route-reserved word (see [`RESERVED_NAMES`]). The | ||
| 17 | //! same rules apply to usernames, repository names, and group names — anywhere a | ||
| 18 | //! name becomes part of a URL path. | ||
| 19 | |||
| 20 | /// The maximum length of a name, in characters (the leading character plus up to | ||
| 21 | /// 38 more, matching the `{0,38}` quantifier). | ||
| 22 | pub const MAX_NAME_LEN: usize = 39; | ||
| 23 | |||
| 24 | /// Names that would collide with a top-level route or a reserved path segment. | ||
| 25 | /// | ||
| 26 | /// Comparison is case-insensitive, so `Admin` is rejected just as `admin` is. | ||
| 27 | pub const RESERVED_NAMES: &[&str] = &[ | ||
| 28 | "api", "assets", "login", "logout", "search", "settings", "admin", "static", "healthz", | ||
| 29 | "invite", "-", | ||
| 30 | ]; | ||
| 31 | |||
| 32 | /// Why a candidate name was rejected. Each variant maps to one actionable | ||
| 33 | /// message; callers surface the `Display` form to the user. | ||
| 34 | #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] | ||
| 35 | pub enum NameError { | ||
| 36 | /// The name was the empty string. | ||
| 37 | #[error("name must not be empty")] | ||
| 38 | Empty, | ||
| 39 | /// The name exceeded [`MAX_NAME_LEN`] characters. | ||
| 40 | #[error("name must be at most {MAX_NAME_LEN} characters")] | ||
| 41 | TooLong, | ||
| 42 | /// The first character was not an ASCII letter or digit. | ||
| 43 | #[error("name must start with an ASCII letter or digit")] | ||
| 44 | BadStart, | ||
| 45 | /// A character after the first was outside `[A-Za-z0-9._-]`. | ||
| 46 | #[error("name may only contain ASCII letters, digits, '.', '_', or '-' (found {0:?})")] | ||
| 47 | BadChar(char), | ||
| 48 | /// The name was `.` or `..`. | ||
| 49 | #[error("name must not be \".\" or \"..\"")] | ||
| 50 | DotName, | ||
| 51 | /// The name ended in `.git`, which would be ambiguous with a bare repo path. | ||
| 52 | #[error("name must not end in \".git\"")] | ||
| 53 | GitSuffix, | ||
| 54 | /// The name ended in `.`. | ||
| 55 | #[error("name must not end in \".\"")] | ||
| 56 | DotSuffix, | ||
| 57 | /// The name matched a reserved word (case-insensitively). | ||
| 58 | #[error("name {0:?} is reserved")] | ||
| 59 | Reserved(String), | ||
| 60 | } | ||
| 61 | |||
| 62 | /// Validate a user, repository, or group name against every rule in this module. | ||
| 63 | /// | ||
| 64 | /// # Errors | ||
| 65 | /// | ||
| 66 | /// Returns the first [`NameError`] that applies. Checks run in a fixed order | ||
| 67 | /// (emptiness, dot-names, length, character set, suffixes, reserved words) so a | ||
| 68 | /// given input always yields the same error. | ||
| 69 | pub fn validate_name(name: &str) -> Result<(), NameError> { | ||
| 70 | if name.is_empty() { | ||
| 71 | return Err(NameError::Empty); | ||
| 72 | } | ||
| 73 | if name == "." || name == ".." { | ||
| 74 | return Err(NameError::DotName); | ||
| 75 | } | ||
| 76 | // ASCII-only past this point is guaranteed by the character checks below, so | ||
| 77 | // counting chars is equivalent to counting bytes for any name that could | ||
| 78 | // pass — but count chars so an over-long multibyte input reports TooLong | ||
| 79 | // rather than a confusing character error. | ||
| 80 | if name.chars().count() > MAX_NAME_LEN { | ||
| 81 | return Err(NameError::TooLong); | ||
| 82 | } | ||
| 83 | |||
| 84 | let mut chars = name.chars(); | ||
| 85 | // `name` is non-empty, so `next` yields. | ||
| 86 | let first = chars.next().unwrap_or('\0'); | ||
| 87 | if !first.is_ascii_alphanumeric() { | ||
| 88 | return Err(NameError::BadStart); | ||
| 89 | } | ||
| 90 | for c in chars { | ||
| 91 | if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) { | ||
| 92 | return Err(NameError::BadChar(c)); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | // Suffix rules. The character checks above guarantee `name` is pure ASCII | ||
| 97 | // here, so byte-slicing for the `.git` comparison lands on a char boundary. | ||
| 98 | if name.ends_with('.') { | ||
| 99 | return Err(NameError::DotSuffix); | ||
| 100 | } | ||
| 101 | if name.len() >= 4 && name[name.len() - 4..].eq_ignore_ascii_case(".git") { | ||
| 102 | return Err(NameError::GitSuffix); | ||
| 103 | } | ||
| 104 | |||
| 105 | let lower = name.to_ascii_lowercase(); | ||
| 106 | if RESERVED_NAMES.contains(&lower.as_str()) { | ||
| 107 | return Err(NameError::Reserved(name.to_string())); | ||
| 108 | } | ||
| 109 | |||
| 110 | Ok(()) | ||
| 111 | } | ||
| 112 | |||
| 113 | #[cfg(test)] | ||
| 114 | mod tests { | ||
| 115 | #![allow(clippy::unwrap_used, clippy::panic)] | ||
| 116 | |||
| 117 | use super::*; | ||
| 118 | |||
| 119 | #[test] | ||
| 120 | fn accepts_ordinary_names() { | ||
| 121 | for name in [ | ||
| 122 | "a", "A", "0", "hanna", "fabrica", "my-repo", "my_repo", "v1.2.3", "a.b_c-d", "Repo123", | ||
| 123 | ] { | ||
| 124 | assert_eq!(validate_name(name), Ok(()), "should accept {name:?}"); | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | #[test] | ||
| 129 | fn rejects_empty() { | ||
| 130 | assert_eq!(validate_name(""), Err(NameError::Empty)); | ||
| 131 | } | ||
| 132 | |||
| 133 | #[test] | ||
| 134 | fn rejects_dot_names() { | ||
| 135 | assert_eq!(validate_name("."), Err(NameError::DotName)); | ||
| 136 | assert_eq!(validate_name(".."), Err(NameError::DotName)); | ||
| 137 | } | ||
| 138 | |||
| 139 | #[test] | ||
| 140 | fn enforces_length_boundary() { | ||
| 141 | let ok = "a".repeat(MAX_NAME_LEN); | ||
| 142 | assert_eq!( | ||
| 143 | validate_name(&ok), | ||
| 144 | Ok(()), | ||
| 145 | "{MAX_NAME_LEN} chars is allowed" | ||
| 146 | ); | ||
| 147 | let too_long = "a".repeat(MAX_NAME_LEN + 1); | ||
| 148 | assert_eq!(validate_name(&too_long), Err(NameError::TooLong)); | ||
| 149 | } | ||
| 150 | |||
| 151 | #[test] | ||
| 152 | fn rejects_bad_start() { | ||
| 153 | for name in ["_foo", "-foo", ".foo", "!foo"] { | ||
| 154 | assert_eq!(validate_name(name), Err(NameError::BadStart), "{name:?}"); | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | #[test] | ||
| 159 | fn rejects_bad_chars() { | ||
| 160 | assert_eq!(validate_name("a b"), Err(NameError::BadChar(' '))); | ||
| 161 | assert_eq!(validate_name("a/b"), Err(NameError::BadChar('/'))); | ||
| 162 | assert_eq!(validate_name("a@b"), Err(NameError::BadChar('@'))); | ||
| 163 | } | ||
| 164 | |||
| 165 | #[test] | ||
| 166 | fn rejects_non_ascii() { | ||
| 167 | // A leading non-ASCII letter fails the start check; an interior one fails | ||
| 168 | // the character check. | ||
| 169 | assert_eq!(validate_name("café"), Err(NameError::BadChar('é'))); | ||
| 170 | assert!(matches!( | ||
| 171 | validate_name("ünsen"), | ||
| 172 | Err(NameError::BadStart | NameError::BadChar(_)) | ||
| 173 | )); | ||
| 174 | } | ||
| 175 | |||
| 176 | #[test] | ||
| 177 | fn rejects_git_suffix_case_insensitively() { | ||
| 178 | for name in ["repo.git", "repo.GIT", "repo.Git", "x.git"] { | ||
| 179 | assert_eq!(validate_name(name), Err(NameError::GitSuffix), "{name:?}"); | ||
| 180 | } | ||
| 181 | // `.git` embedded but not trailing is fine. | ||
| 182 | assert_eq!(validate_name("repo.git.bak"), Ok(())); | ||
| 183 | } | ||
| 184 | |||
| 185 | #[test] | ||
| 186 | fn rejects_trailing_dot() { | ||
| 187 | assert_eq!(validate_name("repo."), Err(NameError::DotSuffix)); | ||
| 188 | } | ||
| 189 | |||
| 190 | #[test] | ||
| 191 | fn rejects_reserved_case_insensitively() { | ||
| 192 | for name in ["admin", "Admin", "ADMIN", "api", "settings", "healthz"] { | ||
| 193 | assert!( | ||
| 194 | matches!(validate_name(name), Err(NameError::Reserved(_))), | ||
| 195 | "{name:?} should be reserved" | ||
| 196 | ); | ||
| 197 | } | ||
| 198 | // A reserved word as a substring is fine. | ||
| 199 | assert_eq!(validate_name("administrator"), Ok(())); | ||
| 200 | } | ||
| 201 | } | ||
crates/store/Cargo.toml +12 −0
| @@ -10,6 +10,18 @@ repository.workspace = true | |||
| 10 | publish.workspace = true | 10 | publish.workspace = true |
| 11 | 11 | ||
| 12 | [dependencies] | 12 | [dependencies] |
| 13 | model = { workspace = true } | ||
| 14 | sqlx = { workspace = true } | ||
| 15 | ulid = { workspace = true } | ||
| 16 | thiserror = { workspace = true } | ||
| 17 | |||
| 18 | [dev-dependencies] | ||
| 19 | tokio = { workspace = true } | ||
| 20 | tempfile = { workspace = true } | ||
| 21 | |||
| 22 | [features] | ||
| 23 | # Opt-in Postgres integration tests, driven by FABRICA_TEST_POSTGRES_URL. | ||
| 24 | postgres-tests = [] | ||
| 13 | 25 | ||
| 14 | [lints] | 26 | [lints] |
| 15 | workspace = true | 27 | workspace = true |
crates/store/src/lib.rs +267 −0
| @@ -3,3 +3,270 @@ | |||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | 4 | ||
| 5 | //! Persistence layer: schema, migrations, and portable queries over SQLite and Postgres. | 5 | //! Persistence layer: schema, migrations, and portable queries over SQLite and Postgres. |
| 6 | //! | ||
| 7 | //! # One code path, two backends | ||
| 8 | //! | ||
| 9 | //! Rather than duplicate every statement per dialect, the store runs over | ||
| 10 | //! [`sqlx::Any`]. That is sound here only because we stay inside a deliberately | ||
| 11 | //! narrow, portable slice of SQL and mediate the two places the backends | ||
| 12 | //! genuinely differ: | ||
| 13 | //! | ||
| 14 | //! * **Placeholders.** Both SQLite and Postgres accept `$1, $2, …` positional | ||
| 15 | //! placeholders (only MySQL needs `?`), so every query is written once with | ||
| 16 | //! `$N`. | ||
| 17 | //! * **Booleans.** Postgres has a real `BOOLEAN`; SQLite stores `INTEGER` 0/1. | ||
| 18 | //! Binding a Rust `bool` encodes correctly for both, but *reading* one back | ||
| 19 | //! differs — a SQLite boolean column decodes as an integer. [`Store::get_bool`] | ||
| 20 | //! is the single place that reconciles this. | ||
| 21 | //! * **Timestamps** are `BIGINT` Unix milliseconds UTC everywhere; **ids** are | ||
| 22 | //! lowercase 26-char ULID `TEXT`, lexicographically sortable so they double as | ||
| 23 | //! the default ordering key. | ||
| 24 | //! | ||
| 25 | //! Migrations live in `migrations/{sqlite,postgres}/NNNN_name.sql`; the matching | ||
| 26 | //! set is embedded at build time and selected at runtime by [`Store::migrate`]. | ||
| 27 | |||
| 28 | mod repos; | ||
| 29 | mod users; | ||
| 30 | |||
| 31 | use std::sync::Once; | ||
| 32 | use std::time::{SystemTime, UNIX_EPOCH}; | ||
| 33 | |||
| 34 | use sqlx::AnyPool; | ||
| 35 | use sqlx::Row; | ||
| 36 | use sqlx::any::{AnyPoolOptions, AnyRow}; | ||
| 37 | use sqlx::migrate::Migrator; | ||
| 38 | |||
| 39 | pub use crate::repos::NewRepo; | ||
| 40 | pub use crate::users::NewUser; | ||
| 41 | |||
| 42 | /// Migrations for the SQLite dialect, embedded at build time. | ||
| 43 | static SQLITE_MIGRATOR: Migrator = sqlx::migrate!("../../migrations/sqlite"); | ||
| 44 | /// Migrations for the PostgreSQL dialect, embedded at build time. | ||
| 45 | static POSTGRES_MIGRATOR: Migrator = sqlx::migrate!("../../migrations/postgres"); | ||
| 46 | |||
| 47 | /// Per-connection SQLite pragmas, applied on every fresh connection. WAL and a | ||
| 48 | /// busy timeout let the CLI and a running server share the file; foreign keys | ||
| 49 | /// must be turned on explicitly on each connection. | ||
| 50 | const SQLITE_PRAGMAS: &[&str] = &[ | ||
| 51 | "PRAGMA journal_mode = WAL", | ||
| 52 | "PRAGMA synchronous = NORMAL", | ||
| 53 | "PRAGMA foreign_keys = ON", | ||
| 54 | "PRAGMA busy_timeout = 5000", | ||
| 55 | ]; | ||
| 56 | |||
| 57 | /// Ensure the `Any` driver knows about the SQLite and Postgres backends. Safe to | ||
| 58 | /// call repeatedly; the work happens exactly once. | ||
| 59 | fn install_drivers() { | ||
| 60 | static ONCE: Once = Once::new(); | ||
| 61 | ONCE.call_once(sqlx::any::install_default_drivers); | ||
| 62 | } | ||
| 63 | |||
| 64 | /// Which concrete database a [`Store`] is talking to. The value is fixed at | ||
| 65 | /// connect time and drives the handful of dialect-dependent decisions. | ||
| 66 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 67 | pub enum Backend { | ||
| 68 | /// A SQLite database (file-backed or in-memory). | ||
| 69 | Sqlite, | ||
| 70 | /// A PostgreSQL database. | ||
| 71 | Postgres, | ||
| 72 | } | ||
| 73 | |||
| 74 | impl Backend { | ||
| 75 | /// Infer the backend from a connection URL's scheme. | ||
| 76 | /// | ||
| 77 | /// # Errors | ||
| 78 | /// | ||
| 79 | /// Returns [`StoreError::UnsupportedUrl`] for any scheme other than | ||
| 80 | /// `sqlite:` or `postgres:`/`postgresql:`. | ||
| 81 | pub fn detect(url: &str) -> Result<Self, StoreError> { | ||
| 82 | if url.starts_with("sqlite:") { | ||
| 83 | Ok(Self::Sqlite) | ||
| 84 | } else if url.starts_with("postgres:") || url.starts_with("postgresql:") { | ||
| 85 | Ok(Self::Postgres) | ||
| 86 | } else { | ||
| 87 | Err(StoreError::UnsupportedUrl { | ||
| 88 | url: url.to_string(), | ||
| 89 | }) | ||
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | /// An error from the persistence layer. | ||
| 95 | #[derive(Debug, thiserror::Error)] | ||
| 96 | pub enum StoreError { | ||
| 97 | /// The connection URL used an unrecognized scheme. | ||
| 98 | #[error("unsupported database URL {url:?}: expected a sqlite: or postgres: scheme")] | ||
| 99 | UnsupportedUrl { | ||
| 100 | /// The offending URL. Connection URLs can carry credentials, so callers | ||
| 101 | /// should avoid surfacing this verbatim to end users. | ||
| 102 | url: String, | ||
| 103 | }, | ||
| 104 | /// A query or connection failed. Boxed to keep [`StoreError`] small. | ||
| 105 | #[error(transparent)] | ||
| 106 | Query(#[from] Box<sqlx::Error>), | ||
| 107 | /// A migration failed to apply. | ||
| 108 | #[error(transparent)] | ||
| 109 | Migrate(#[from] sqlx::migrate::MigrateError), | ||
| 110 | /// A name failed validation before it could be written. | ||
| 111 | #[error("invalid name: {0}")] | ||
| 112 | Name(#[from] model::NameError), | ||
| 113 | /// A uniqueness constraint was violated (e.g. a duplicate username). | ||
| 114 | #[error("{entity} already exists with that {field}")] | ||
| 115 | Conflict { | ||
| 116 | /// The kind of entity that collided (`"user"`, `"repo"`, …). | ||
| 117 | entity: &'static str, | ||
| 118 | /// The field whose uniqueness was violated. | ||
| 119 | field: &'static str, | ||
| 120 | }, | ||
| 121 | } | ||
| 122 | |||
| 123 | impl From<sqlx::Error> for StoreError { | ||
| 124 | fn from(err: sqlx::Error) -> Self { | ||
| 125 | Self::Query(Box::new(err)) | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | /// A handle to the database: a connection pool plus the backend it targets. | ||
| 130 | /// | ||
| 131 | /// Cloneable and cheap to share — the underlying pool is reference-counted. | ||
| 132 | #[derive(Debug, Clone)] | ||
| 133 | pub struct Store { | ||
| 134 | pool: AnyPool, | ||
| 135 | backend: Backend, | ||
| 136 | } | ||
| 137 | |||
| 138 | impl Store { | ||
| 139 | /// Open a pool against `url`, applying SQLite pragmas when appropriate. | ||
| 140 | /// | ||
| 141 | /// File-backed SQLite URLs are created if missing. This does **not** run | ||
| 142 | /// migrations — call [`Store::migrate`] separately so `--no-migrate` can skip | ||
| 143 | /// it. | ||
| 144 | /// | ||
| 145 | /// # Errors | ||
| 146 | /// | ||
| 147 | /// Returns [`StoreError::UnsupportedUrl`] for an unknown scheme, or | ||
| 148 | /// [`StoreError::Query`] if the pool cannot be established. | ||
| 149 | pub async fn connect(url: &str, max_connections: u32) -> Result<Self, StoreError> { | ||
| 150 | install_drivers(); | ||
| 151 | let backend = Backend::detect(url)?; | ||
| 152 | let url = normalize_url(url, backend); | ||
| 153 | |||
| 154 | let mut options = AnyPoolOptions::new().max_connections(max_connections.max(1)); | ||
| 155 | if backend == Backend::Sqlite { | ||
| 156 | options = options.after_connect(|conn, _meta| { | ||
| 157 | Box::pin(async move { | ||
| 158 | for pragma in SQLITE_PRAGMAS { | ||
| 159 | sqlx::query(*pragma).execute(&mut *conn).await?; | ||
| 160 | } | ||
| 161 | Ok(()) | ||
| 162 | }) | ||
| 163 | }); | ||
| 164 | } | ||
| 165 | |||
| 166 | let pool = options.connect(&url).await.map_err(Box::new)?; | ||
| 167 | Ok(Self { pool, backend }) | ||
| 168 | } | ||
| 169 | |||
| 170 | /// Wrap an already-open pool. Primarily a test seam. | ||
| 171 | /// | ||
| 172 | /// The `backend` must match the pool's actual driver, or dialect-dependent | ||
| 173 | /// decoding (booleans) will misbehave. | ||
| 174 | #[must_use] | ||
| 175 | pub fn from_pool(pool: AnyPool, backend: Backend) -> Self { | ||
| 176 | Self { pool, backend } | ||
| 177 | } | ||
| 178 | |||
| 179 | /// The backend this store targets. | ||
| 180 | #[must_use] | ||
| 181 | pub fn backend(&self) -> Backend { | ||
| 182 | self.backend | ||
| 183 | } | ||
| 184 | |||
| 185 | /// The underlying connection pool, for callers that need raw access. | ||
| 186 | #[must_use] | ||
| 187 | pub fn pool(&self) -> &AnyPool { | ||
| 188 | &self.pool | ||
| 189 | } | ||
| 190 | |||
| 191 | /// Apply all pending migrations for this backend's dialect. | ||
| 192 | /// | ||
| 193 | /// # Errors | ||
| 194 | /// | ||
| 195 | /// Returns [`StoreError::Migrate`] if a migration fails or the recorded | ||
| 196 | /// history is inconsistent with the embedded set. | ||
| 197 | pub async fn migrate(&self) -> Result<(), StoreError> { | ||
| 198 | let migrator = match self.backend { | ||
| 199 | Backend::Sqlite => &SQLITE_MIGRATOR, | ||
| 200 | Backend::Postgres => &POSTGRES_MIGRATOR, | ||
| 201 | }; | ||
| 202 | migrator.run(&self.pool).await?; | ||
| 203 | Ok(()) | ||
| 204 | } | ||
| 205 | |||
| 206 | /// Read a boolean column, bridging SQLite's integer storage and Postgres's | ||
| 207 | /// native `BOOLEAN`. This is the one place the two dialects' boolean | ||
| 208 | /// representations are reconciled on the read path. | ||
| 209 | fn get_bool(&self, row: &AnyRow, column: &str) -> Result<bool, sqlx::Error> { | ||
| 210 | match self.backend { | ||
| 211 | Backend::Sqlite => Ok(row.try_get::<i64, _>(column)? != 0), | ||
| 212 | Backend::Postgres => row.try_get::<bool, _>(column), | ||
| 213 | } | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | /// Generate a fresh identifier: a lowercase, 26-character ULID. ULIDs are | ||
| 218 | /// lexicographically sortable by creation time, so they double as an ordering | ||
| 219 | /// key and never need a separate sequence. | ||
| 220 | fn new_id() -> String { | ||
| 221 | ulid::Ulid::new().to_string().to_lowercase() | ||
| 222 | } | ||
| 223 | |||
| 224 | /// The current time as Unix milliseconds UTC, matching the schema's `BIGINT` | ||
| 225 | /// timestamp columns. Clamps rather than panicking on the impossible cases (a | ||
| 226 | /// clock before the epoch, or a time past year 292-million). | ||
| 227 | fn now_ms() -> i64 { | ||
| 228 | SystemTime::now() | ||
| 229 | .duration_since(UNIX_EPOCH) | ||
| 230 | .map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX)) | ||
| 231 | } | ||
| 232 | |||
| 233 | /// Ensure a file-backed SQLite URL will create its database if absent. In-memory | ||
| 234 | /// URLs and any URL that already specifies a `mode` are left untouched. | ||
| 235 | fn normalize_url(url: &str, backend: Backend) -> String { | ||
| 236 | if backend == Backend::Sqlite && !url.contains(":memory:") && !url.contains("mode=") { | ||
| 237 | let separator = if url.contains('?') { '&' } else { '?' }; | ||
| 238 | format!("{url}{separator}mode=rwc") | ||
| 239 | } else { | ||
| 240 | url.to_string() | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | /// Map a failed write to [`StoreError::Conflict`] when it was a uniqueness | ||
| 245 | /// violation, attributing it to the first matching field. Any other error is | ||
| 246 | /// passed through unchanged. | ||
| 247 | /// | ||
| 248 | /// `needles` pairs a lowercase substring to look for in the violated | ||
| 249 | /// constraint's name/message with the field label to report. The database's | ||
| 250 | /// error text names the offending column (`users.email_lower`, or a Postgres | ||
| 251 | /// constraint like `users_email_lower_key`), so a substring match reliably | ||
| 252 | /// identifies the field across both dialects. | ||
| 253 | fn map_conflict( | ||
| 254 | err: sqlx::Error, | ||
| 255 | entity: &'static str, | ||
| 256 | needles: &[(&str, &'static str)], | ||
| 257 | ) -> StoreError { | ||
| 258 | if let sqlx::Error::Database(db) = &err | ||
| 259 | && db.is_unique_violation() | ||
| 260 | { | ||
| 261 | let haystack = format!("{} {}", db.constraint().unwrap_or(""), db.message()).to_lowercase(); | ||
| 262 | let field = needles | ||
| 263 | .iter() | ||
| 264 | .find(|(needle, _)| haystack.contains(needle)) | ||
| 265 | .map_or("value", |(_, field)| *field); | ||
| 266 | return StoreError::Conflict { entity, field }; | ||
| 267 | } | ||
| 268 | err.into() | ||
| 269 | } | ||
| 270 | |||
| 271 | #[cfg(test)] | ||
| 272 | mod tests; | ||
crates/store/src/repos.rs +127 −0
| @@ -0,0 +1,127 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! Repositories: creation and path lookup. | ||
| 6 | |||
| 7 | use model::Repo; | ||
| 8 | use sqlx::any::AnyRow; | ||
| 9 | use sqlx::{AssertSqlSafe, Row}; | ||
| 10 | |||
| 11 | use crate::{Store, StoreError, map_conflict, new_id, now_ms}; | ||
| 12 | |||
| 13 | /// The columns of `repos` in a fixed order, shared by every `SELECT`. | ||
| 14 | const REPO_COLUMNS: &str = "id, owner_id, group_id, name, path, description, is_private, \ | ||
| 15 | default_branch, archived_at, size_bytes, pushed_at, created_at, updated_at"; | ||
| 16 | |||
| 17 | /// Fields needed to create a repository. The server fills in the id, timestamps, | ||
| 18 | /// and the derived size/push bookkeeping. | ||
| 19 | #[derive(Debug, Clone)] | ||
| 20 | pub struct NewRepo { | ||
| 21 | /// Owning user id. | ||
| 22 | pub owner_id: String, | ||
| 23 | /// Containing group id, or `None` when the repo hangs directly off the owner. | ||
| 24 | pub group_id: Option<String>, | ||
| 25 | /// The repo's own name; validated with [`model::validate_name`]. | ||
| 26 | pub name: String, | ||
| 27 | /// Materialized full path within the owner (`group/sub/repo`, or just `name`). | ||
| 28 | pub path: String, | ||
| 29 | /// Optional one-line description. | ||
| 30 | pub description: Option<String>, | ||
| 31 | /// Whether the repo is private (invisible to non-collaborators). | ||
| 32 | pub is_private: bool, | ||
| 33 | /// The repo's default branch. | ||
| 34 | pub default_branch: String, | ||
| 35 | } | ||
| 36 | |||
| 37 | impl Store { | ||
| 38 | /// Create a repository, returning the persisted row. | ||
| 39 | /// | ||
| 40 | /// # Errors | ||
| 41 | /// | ||
| 42 | /// * [`StoreError::Name`] if the repo name is invalid. | ||
| 43 | /// * [`StoreError::Conflict`] if the owner already has a repo at that path. | ||
| 44 | /// * [`StoreError::Query`] for any other database failure. | ||
| 45 | pub async fn create_repo(&self, new: NewRepo) -> Result<Repo, StoreError> { | ||
| 46 | model::validate_name(&new.name)?; | ||
| 47 | let now = now_ms(); | ||
| 48 | let repo = Repo { | ||
| 49 | id: new_id(), | ||
| 50 | owner_id: new.owner_id, | ||
| 51 | group_id: new.group_id, | ||
| 52 | name: new.name, | ||
| 53 | path: new.path, | ||
| 54 | description: new.description, | ||
| 55 | is_private: new.is_private, | ||
| 56 | default_branch: new.default_branch, | ||
| 57 | archived_at: None, | ||
| 58 | size_bytes: 0, | ||
| 59 | pushed_at: None, | ||
| 60 | created_at: now, | ||
| 61 | updated_at: now, | ||
| 62 | }; | ||
| 63 | |||
| 64 | let sql = "INSERT INTO repos \ | ||
| 65 | (id, owner_id, group_id, name, path, description, is_private, default_branch, \ | ||
| 66 | archived_at, size_bytes, pushed_at, created_at, updated_at) \ | ||
| 67 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)"; | ||
| 68 | sqlx::query(sql) | ||
| 69 | .bind(repo.id.clone()) | ||
| 70 | .bind(repo.owner_id.clone()) | ||
| 71 | .bind(repo.group_id.clone()) | ||
| 72 | .bind(repo.name.clone()) | ||
| 73 | .bind(repo.path.clone()) | ||
| 74 | .bind(repo.description.clone()) | ||
| 75 | .bind(repo.is_private) | ||
| 76 | .bind(repo.default_branch.clone()) | ||
| 77 | .bind(repo.archived_at) | ||
| 78 | .bind(repo.size_bytes) | ||
| 79 | .bind(repo.pushed_at) | ||
| 80 | .bind(repo.created_at) | ||
| 81 | .bind(repo.updated_at) | ||
| 82 | .execute(&self.pool) | ||
| 83 | .await | ||
| 84 | .map_err(|e| map_conflict(e, "repo", &[("path", "path")]))?; | ||
| 85 | |||
| 86 | Ok(repo) | ||
| 87 | } | ||
| 88 | |||
| 89 | /// Resolve a repository by `(owner_id, path)`, or `None` if absent. This is | ||
| 90 | /// the authoritative name→repo lookup; the on-disk tree is keyed by id. | ||
| 91 | /// | ||
| 92 | /// # Errors | ||
| 93 | /// | ||
| 94 | /// Returns [`StoreError::Query`] on a database failure. | ||
| 95 | pub async fn repo_by_owner_path( | ||
| 96 | &self, | ||
| 97 | owner_id: &str, | ||
| 98 | path: &str, | ||
| 99 | ) -> Result<Option<Repo>, StoreError> { | ||
| 100 | let sql = format!("SELECT {REPO_COLUMNS} FROM repos WHERE owner_id = $1 AND path = $2"); | ||
| 101 | let row = sqlx::query(AssertSqlSafe(sql)) | ||
| 102 | .bind(owner_id) | ||
| 103 | .bind(path) | ||
| 104 | .fetch_optional(&self.pool) | ||
| 105 | .await?; | ||
| 106 | Ok(row.as_ref().map(|r| self.map_repo(r)).transpose()?) | ||
| 107 | } | ||
| 108 | |||
| 109 | /// Map a `repos` row (selected via [`REPO_COLUMNS`]) to a [`Repo`]. | ||
| 110 | fn map_repo(&self, row: &AnyRow) -> Result<Repo, sqlx::Error> { | ||
| 111 | Ok(Repo { | ||
| 112 | id: row.try_get("id")?, | ||
| 113 | owner_id: row.try_get("owner_id")?, | ||
| 114 | group_id: row.try_get("group_id")?, | ||
| 115 | name: row.try_get("name")?, | ||
| 116 | path: row.try_get("path")?, | ||
| 117 | description: row.try_get("description")?, | ||
| 118 | is_private: self.get_bool(row, "is_private")?, | ||
| 119 | default_branch: row.try_get("default_branch")?, | ||
| 120 | archived_at: row.try_get("archived_at")?, | ||
| 121 | size_bytes: row.try_get("size_bytes")?, | ||
| 122 | pushed_at: row.try_get("pushed_at")?, | ||
| 123 | created_at: row.try_get("created_at")?, | ||
| 124 | updated_at: row.try_get("updated_at")?, | ||
| 125 | }) | ||
| 126 | } | ||
| 127 | } | ||
crates/store/src/tests.rs +424 −0
| @@ -0,0 +1,424 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! Store tests. The portable behaviour runs against SQLite both in-memory and | ||
| 6 | //! file-backed; the same round-trips run against Postgres when the | ||
| 7 | //! `postgres-tests` feature is enabled and `FABRICA_TEST_POSTGRES_URL` is set. | ||
| 8 | |||
| 9 | #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] | ||
| 10 | |||
| 11 | use tempfile::TempDir; | ||
| 12 | |||
| 13 | use super::{Backend, NewRepo, NewUser, Store, StoreError}; | ||
| 14 | |||
| 15 | /// A migrated store plus any temp directory that must outlive it. | ||
| 16 | struct Fixture { | ||
| 17 | store: Store, | ||
| 18 | _dir: Option<TempDir>, | ||
| 19 | } | ||
| 20 | |||
| 21 | /// A migrated in-memory SQLite store. A single connection keeps the database | ||
| 22 | /// alive for the life of the pool. | ||
| 23 | async fn memory_store() -> Fixture { | ||
| 24 | let store = Store::connect("sqlite::memory:", 1).await.unwrap(); | ||
| 25 | store.migrate().await.unwrap(); | ||
| 26 | Fixture { store, _dir: None } | ||
| 27 | } | ||
| 28 | |||
| 29 | /// A migrated file-backed SQLite store in a fresh temp directory. | ||
| 30 | async fn file_store() -> Fixture { | ||
| 31 | let dir = tempfile::tempdir().unwrap(); | ||
| 32 | let url = format!("sqlite://{}", dir.path().join("fabrica.db").display()); | ||
| 33 | let store = Store::connect(&url, 5).await.unwrap(); | ||
| 34 | store.migrate().await.unwrap(); | ||
| 35 | Fixture { | ||
| 36 | store, | ||
| 37 | _dir: Some(dir), | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | /// Both SQLite flavours the portable tests must pass against. | ||
| 42 | async fn sqlite_fixtures() -> Vec<Fixture> { | ||
| 43 | vec![memory_store().await, file_store().await] | ||
| 44 | } | ||
| 45 | |||
| 46 | fn sample_user(username: &str, email: &str) -> NewUser { | ||
| 47 | NewUser { | ||
| 48 | username: username.to_string(), | ||
| 49 | email: email.to_string(), | ||
| 50 | display_name: Some("Sample".to_string()), | ||
| 51 | password_hash: None, | ||
| 52 | is_admin: false, | ||
| 53 | must_change_password: false, | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | #[tokio::test] | ||
| 58 | async fn migrations_are_idempotent() { | ||
| 59 | for fx in sqlite_fixtures().await { | ||
| 60 | // A second run over the already-migrated database is a no-op. | ||
| 61 | fx.store.migrate().await.unwrap(); | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | #[tokio::test] | ||
| 66 | async fn create_and_fetch_user() { | ||
| 67 | for fx in sqlite_fixtures().await { | ||
| 68 | let created = fx | ||
| 69 | .store | ||
| 70 | .create_user(sample_user("hanna", "hanna@example.com")) | ||
| 71 | .await | ||
| 72 | .unwrap(); | ||
| 73 | assert_eq!(created.id.len(), 26, "id should be a 26-char ULID"); | ||
| 74 | assert_eq!(created.created_at, created.updated_at); | ||
| 75 | |||
| 76 | let by_id = fx.store.user_by_id(&created.id).await.unwrap().unwrap(); | ||
| 77 | assert_eq!(by_id, created); | ||
| 78 | |||
| 79 | // Username lookup is case-insensitive. | ||
| 80 | let by_name = fx.store.user_by_username("HANNA").await.unwrap().unwrap(); | ||
| 81 | assert_eq!(by_name, created); | ||
| 82 | |||
| 83 | assert!(fx.store.user_by_id("nope").await.unwrap().is_none()); | ||
| 84 | assert!(fx.store.user_by_username("ghost").await.unwrap().is_none()); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | #[tokio::test] | ||
| 89 | async fn duplicate_username_conflicts_case_insensitively() { | ||
| 90 | for fx in sqlite_fixtures().await { | ||
| 91 | fx.store | ||
| 92 | .create_user(sample_user("dup", "a@example.com")) | ||
| 93 | .await | ||
| 94 | .unwrap(); | ||
| 95 | let err = fx | ||
| 96 | .store | ||
| 97 | .create_user(sample_user("DUP", "b@example.com")) | ||
| 98 | .await | ||
| 99 | .unwrap_err(); | ||
| 100 | assert!( | ||
| 101 | matches!( | ||
| 102 | err, | ||
| 103 | StoreError::Conflict { | ||
| 104 | entity: "user", | ||
| 105 | field: "username" | ||
| 106 | } | ||
| 107 | ), | ||
| 108 | "got {err:?}" | ||
| 109 | ); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | #[tokio::test] | ||
| 114 | async fn duplicate_email_conflicts_case_insensitively() { | ||
| 115 | for fx in sqlite_fixtures().await { | ||
| 116 | fx.store | ||
| 117 | .create_user(sample_user("one", "same@example.com")) | ||
| 118 | .await | ||
| 119 | .unwrap(); | ||
| 120 | let err = fx | ||
| 121 | .store | ||
| 122 | .create_user(sample_user("two", "SAME@example.com")) | ||
| 123 | .await | ||
| 124 | .unwrap_err(); | ||
| 125 | assert!( | ||
| 126 | matches!( | ||
| 127 | err, | ||
| 128 | StoreError::Conflict { | ||
| 129 | entity: "user", | ||
| 130 | field: "email" | ||
| 131 | } | ||
| 132 | ), | ||
| 133 | "got {err:?}" | ||
| 134 | ); | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | #[tokio::test] | ||
| 139 | async fn invalid_username_is_rejected_before_insert() { | ||
| 140 | for fx in sqlite_fixtures().await { | ||
| 141 | let err = fx | ||
| 142 | .store | ||
| 143 | .create_user(sample_user("bad name", "x@example.com")) | ||
| 144 | .await | ||
| 145 | .unwrap_err(); | ||
| 146 | assert!(matches!(err, StoreError::Name(_)), "got {err:?}"); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | #[tokio::test] | ||
| 151 | async fn booleans_round_trip() { | ||
| 152 | for fx in sqlite_fixtures().await { | ||
| 153 | let boss = fx | ||
| 154 | .store | ||
| 155 | .create_user(NewUser { | ||
| 156 | is_admin: true, | ||
| 157 | must_change_password: true, | ||
| 158 | ..sample_user("boss", "boss@example.com") | ||
| 159 | }) | ||
| 160 | .await | ||
| 161 | .unwrap(); | ||
| 162 | assert!(boss.is_admin); | ||
| 163 | assert!(boss.must_change_password); | ||
| 164 | |||
| 165 | let fetched = fx.store.user_by_id(&boss.id).await.unwrap().unwrap(); | ||
| 166 | assert!(fetched.is_admin, "is_admin must survive the round-trip"); | ||
| 167 | assert!(fetched.must_change_password); | ||
| 168 | |||
| 169 | // And the default-false path. | ||
| 170 | let plain = fx.store.user_by_username("boss").await.unwrap(); | ||
| 171 | assert!(plain.is_some()); | ||
| 172 | let normal = fx | ||
| 173 | .store | ||
| 174 | .create_user(sample_user("normal", "normal@example.com")) | ||
| 175 | .await | ||
| 176 | .unwrap(); | ||
| 177 | let normal = fx.store.user_by_id(&normal.id).await.unwrap().unwrap(); | ||
| 178 | assert!(!normal.is_admin); | ||
| 179 | assert!(!normal.must_change_password); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | #[tokio::test] | ||
| 184 | async fn create_and_fetch_repo() { | ||
| 185 | for fx in sqlite_fixtures().await { | ||
| 186 | let owner = fx | ||
| 187 | .store | ||
| 188 | .create_user(sample_user("owner", "owner@example.com")) | ||
| 189 | .await | ||
| 190 | .unwrap(); | ||
| 191 | |||
| 192 | let repo = fx | ||
| 193 | .store | ||
| 194 | .create_repo(NewRepo { | ||
| 195 | owner_id: owner.id.clone(), | ||
| 196 | group_id: None, | ||
| 197 | name: "project".to_string(), | ||
| 198 | path: "project".to_string(), | ||
| 199 | description: Some("a repo".to_string()), | ||
| 200 | is_private: false, | ||
| 201 | default_branch: "main".to_string(), | ||
| 202 | }) | ||
| 203 | .await | ||
| 204 | .unwrap(); | ||
| 205 | assert_eq!(repo.size_bytes, 0); | ||
| 206 | assert!(repo.pushed_at.is_none()); | ||
| 207 | assert!(!repo.is_private); | ||
| 208 | |||
| 209 | let fetched = fx | ||
| 210 | .store | ||
| 211 | .repo_by_owner_path(&owner.id, "project") | ||
| 212 | .await | ||
| 213 | .unwrap() | ||
| 214 | .unwrap(); | ||
| 215 | assert_eq!(fetched, repo); | ||
| 216 | |||
| 217 | // Same owner, same path collides. | ||
| 218 | let err = fx | ||
| 219 | .store | ||
| 220 | .create_repo(NewRepo { | ||
| 221 | owner_id: owner.id.clone(), | ||
| 222 | group_id: None, | ||
| 223 | name: "project".to_string(), | ||
| 224 | path: "project".to_string(), | ||
| 225 | description: None, | ||
| 226 | is_private: true, | ||
| 227 | default_branch: "main".to_string(), | ||
| 228 | }) | ||
| 229 | .await | ||
| 230 | .unwrap_err(); | ||
| 231 | assert!( | ||
| 232 | matches!( | ||
| 233 | err, | ||
| 234 | StoreError::Conflict { | ||
| 235 | entity: "repo", | ||
| 236 | field: "path" | ||
| 237 | } | ||
| 238 | ), | ||
| 239 | "got {err:?}" | ||
| 240 | ); | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | #[tokio::test] | ||
| 245 | async fn foreign_keys_are_enforced() { | ||
| 246 | // A repo whose owner does not exist must be rejected — proof the SQLite | ||
| 247 | // `foreign_keys` pragma took effect on the pooled connection. | ||
| 248 | for fx in sqlite_fixtures().await { | ||
| 249 | let err = fx | ||
| 250 | .store | ||
| 251 | .create_repo(NewRepo { | ||
| 252 | owner_id: "no-such-owner".to_string(), | ||
| 253 | group_id: None, | ||
| 254 | name: "orphan".to_string(), | ||
| 255 | path: "orphan".to_string(), | ||
| 256 | description: None, | ||
| 257 | is_private: true, | ||
| 258 | default_branch: "main".to_string(), | ||
| 259 | }) | ||
| 260 | .await | ||
| 261 | .unwrap_err(); | ||
| 262 | assert!( | ||
| 263 | matches!(err, StoreError::Query(_)), | ||
| 264 | "expected a database (FK) error, got {err:?}" | ||
| 265 | ); | ||
| 266 | } | ||
| 267 | } | ||
| 268 | |||
| 269 | #[test] | ||
| 270 | fn unsupported_url_is_rejected() { | ||
| 271 | let err = Backend::detect("mysql://localhost/db").unwrap_err(); | ||
| 272 | assert!( | ||
| 273 | matches!(err, StoreError::UnsupportedUrl { .. }), | ||
| 274 | "got {err:?}" | ||
| 275 | ); | ||
| 276 | assert_eq!(Backend::detect("sqlite::memory:").unwrap(), Backend::Sqlite); | ||
| 277 | assert_eq!( | ||
| 278 | Backend::detect("postgres://localhost/db").unwrap(), | ||
| 279 | Backend::Postgres | ||
| 280 | ); | ||
| 281 | assert_eq!( | ||
| 282 | Backend::detect("postgresql://localhost/db").unwrap(), | ||
| 283 | Backend::Postgres | ||
| 284 | ); | ||
| 285 | } | ||
| 286 | |||
| 287 | /// The two dialects' initial migrations must define the same tables with the | ||
| 288 | /// same columns in the same order — a hermetic check that needs no database. | ||
| 289 | /// The only sanctioned differences are the boolean column type and dialect | ||
| 290 | /// punctuation. | ||
| 291 | #[test] | ||
| 292 | fn schema_files_are_equivalent() { | ||
| 293 | let sqlite = include_str!("../../../migrations/sqlite/0001_init.sql"); | ||
| 294 | let postgres = include_str!("../../../migrations/postgres/0001_init.sql"); | ||
| 295 | |||
| 296 | assert_eq!( | ||
| 297 | columns_by_table(sqlite), | ||
| 298 | columns_by_table(postgres), | ||
| 299 | "SQLite and Postgres schemas must define identical tables and columns" | ||
| 300 | ); | ||
| 301 | |||
| 302 | // The boolean columns must use the dialect-appropriate storage type. | ||
| 303 | for column in ["must_change_password", "is_admin", "is_private"] { | ||
| 304 | assert!( | ||
| 305 | column_line(sqlite, column).contains("INTEGER"), | ||
| 306 | "SQLite {column} should be INTEGER" | ||
| 307 | ); | ||
| 308 | assert!( | ||
| 309 | column_line(postgres, column).contains("BOOLEAN"), | ||
| 310 | "Postgres {column} should be BOOLEAN" | ||
| 311 | ); | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | /// Extract, per table, the ordered list of column names from a migration file. | ||
| 316 | /// Relies on the house style: each column on its own line beginning with the | ||
| 317 | /// column name, table-level constraints beginning with an uppercase keyword. | ||
| 318 | fn columns_by_table(sql: &str) -> std::collections::BTreeMap<String, Vec<String>> { | ||
| 319 | const CONSTRAINT_KEYWORDS: &[&str] = &["UNIQUE", "PRIMARY", "FOREIGN", "CHECK", "CONSTRAINT"]; | ||
| 320 | let mut tables = std::collections::BTreeMap::new(); | ||
| 321 | let mut current: Option<(String, Vec<String>)> = None; | ||
| 322 | |||
| 323 | for raw in sql.lines() { | ||
| 324 | let line = raw.trim(); | ||
| 325 | if let Some(rest) = line.strip_prefix("CREATE TABLE ") { | ||
| 326 | let name = rest.trim_end_matches(" (").trim_end_matches('(').trim(); | ||
| 327 | current = Some((name.to_string(), Vec::new())); | ||
| 328 | continue; | ||
| 329 | } | ||
| 330 | let Some((name, cols)) = current.as_mut() else { | ||
| 331 | continue; | ||
| 332 | }; | ||
| 333 | if line.starts_with(')') { | ||
| 334 | let (name, cols) = current.take().expect("current table"); | ||
| 335 | tables.insert(name, cols); | ||
| 336 | continue; | ||
| 337 | } | ||
| 338 | // A column line starts with an identifier; skip blanks, comments, and | ||
| 339 | // table-level constraints. | ||
| 340 | let first = line.split([' ', '\t']).next().unwrap_or(""); | ||
| 341 | if first.is_empty() | ||
| 342 | || line.starts_with("--") | ||
| 343 | || CONSTRAINT_KEYWORDS.contains(&first.to_ascii_uppercase().as_str()) | ||
| 344 | { | ||
| 345 | let _ = name; | ||
| 346 | continue; | ||
| 347 | } | ||
| 348 | cols.push(first.to_string()); | ||
| 349 | } | ||
| 350 | tables | ||
| 351 | } | ||
| 352 | |||
| 353 | /// Return the first line defining `column` (for type assertions). | ||
| 354 | fn column_line<'a>(sql: &'a str, column: &str) -> &'a str { | ||
| 355 | sql.lines() | ||
| 356 | .map(str::trim) | ||
| 357 | .find(|line| line.split([' ', '\t']).next() == Some(column)) | ||
| 358 | .unwrap_or("") | ||
| 359 | } | ||
| 360 | |||
| 361 | /// Postgres round-trips, mirroring the SQLite ones. Ignored unless the | ||
| 362 | /// `postgres-tests` feature is on and `FABRICA_TEST_POSTGRES_URL` points at a | ||
| 363 | /// reachable database; the test cleans up the rows it creates so it can rerun. | ||
| 364 | #[tokio::test] | ||
| 365 | #[cfg_attr( | ||
| 366 | not(feature = "postgres-tests"), | ||
| 367 | ignore = "enable the postgres-tests feature and set FABRICA_TEST_POSTGRES_URL" | ||
| 368 | )] | ||
| 369 | async fn postgres_round_trip() { | ||
| 370 | let Ok(url) = std::env::var("FABRICA_TEST_POSTGRES_URL") else { | ||
| 371 | eprintln!("skipping postgres_round_trip: FABRICA_TEST_POSTGRES_URL not set"); | ||
| 372 | return; | ||
| 373 | }; | ||
| 374 | |||
| 375 | let store = Store::connect(&url, 5).await.unwrap(); | ||
| 376 | assert_eq!(store.backend(), Backend::Postgres); | ||
| 377 | store.migrate().await.unwrap(); | ||
| 378 | |||
| 379 | // Unique names so repeated runs against a shared database don't collide. | ||
| 380 | let suffix = super::new_id(); | ||
| 381 | let username = format!("pg{suffix}"); | ||
| 382 | let email = format!("{suffix}@example.com"); | ||
| 383 | |||
| 384 | let admin = store | ||
| 385 | .create_user(NewUser { | ||
| 386 | is_admin: true, | ||
| 387 | must_change_password: true, | ||
| 388 | ..sample_user(&username, &email) | ||
| 389 | }) | ||
| 390 | .await | ||
| 391 | .unwrap(); | ||
| 392 | |||
| 393 | let fetched = store.user_by_id(&admin.id).await.unwrap().unwrap(); | ||
| 394 | assert_eq!(fetched, admin); | ||
| 395 | assert!(fetched.is_admin, "native BOOLEAN must decode as true"); | ||
| 396 | assert!(fetched.must_change_password); | ||
| 397 | |||
| 398 | let repo = store | ||
| 399 | .create_repo(NewRepo { | ||
| 400 | owner_id: admin.id.clone(), | ||
| 401 | group_id: None, | ||
| 402 | name: "pgproj".to_string(), | ||
| 403 | path: "pgproj".to_string(), | ||
| 404 | description: None, | ||
| 405 | is_private: false, | ||
| 406 | default_branch: "main".to_string(), | ||
| 407 | }) | ||
| 408 | .await | ||
| 409 | .unwrap(); | ||
| 410 | let fetched_repo = store | ||
| 411 | .repo_by_owner_path(&admin.id, "pgproj") | ||
| 412 | .await | ||
| 413 | .unwrap() | ||
| 414 | .unwrap(); | ||
| 415 | assert_eq!(fetched_repo, repo); | ||
| 416 | assert!(!fetched_repo.is_private); | ||
| 417 | |||
| 418 | // Clean up (repo cascades with the user). | ||
| 419 | sqlx::query("DELETE FROM users WHERE id = $1") | ||
| 420 | .bind(&admin.id) | ||
| 421 | .execute(store.pool()) | ||
| 422 | .await | ||
| 423 | .unwrap(); | ||
| 424 | } | ||
crates/store/src/users.rs +132 −0
| @@ -0,0 +1,132 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! User accounts: creation and lookup. | ||
| 6 | |||
| 7 | use model::User; | ||
| 8 | use sqlx::any::AnyRow; | ||
| 9 | use sqlx::{AssertSqlSafe, Row}; | ||
| 10 | |||
| 11 | use crate::{Store, StoreError, map_conflict, new_id, now_ms}; | ||
| 12 | |||
| 13 | /// The columns of `users` in a fixed order, shared by every `SELECT` so the | ||
| 14 | /// row-mapping in [`Store::map_user`] can rely on names. | ||
| 15 | const USER_COLUMNS: &str = "id, username, email, display_name, password_hash, \ | ||
| 16 | must_change_password, is_admin, disabled_at, created_at, updated_at"; | ||
| 17 | |||
| 18 | /// Fields needed to create a user. The server fills in the id, timestamps, and | ||
| 19 | /// the normalized `*_lower` uniqueness keys. | ||
| 20 | #[derive(Debug, Clone)] | ||
| 21 | pub struct NewUser { | ||
| 22 | /// Login name; validated with [`model::validate_name`] before insert. | ||
| 23 | pub username: String, | ||
| 24 | /// Primary email address. | ||
| 25 | pub email: String, | ||
| 26 | /// Optional display name. | ||
| 27 | pub display_name: Option<String>, | ||
| 28 | /// Argon2id PHC hash, or `None` for an account awaiting invite completion. | ||
| 29 | pub password_hash: Option<String>, | ||
| 30 | /// Whether the account is a site administrator. | ||
| 31 | pub is_admin: bool, | ||
| 32 | /// Whether the user must change their password on next login. | ||
| 33 | pub must_change_password: bool, | ||
| 34 | } | ||
| 35 | |||
| 36 | impl Store { | ||
| 37 | /// Create a user, returning the persisted row. | ||
| 38 | /// | ||
| 39 | /// The username is validated and case-folded for uniqueness; the email is | ||
| 40 | /// likewise unique case-insensitively. | ||
| 41 | /// | ||
| 42 | /// # Errors | ||
| 43 | /// | ||
| 44 | /// * [`StoreError::Name`] if the username is invalid. | ||
| 45 | /// * [`StoreError::Conflict`] if the username or email is already taken. | ||
| 46 | /// * [`StoreError::Query`] for any other database failure. | ||
| 47 | pub async fn create_user(&self, new: NewUser) -> Result<User, StoreError> { | ||
| 48 | model::validate_name(&new.username)?; | ||
| 49 | let now = now_ms(); | ||
| 50 | let user = User { | ||
| 51 | id: new_id(), | ||
| 52 | username: new.username, | ||
| 53 | email: new.email, | ||
| 54 | display_name: new.display_name, | ||
| 55 | password_hash: new.password_hash, | ||
| 56 | must_change_password: new.must_change_password, | ||
| 57 | is_admin: new.is_admin, | ||
| 58 | disabled_at: None, | ||
| 59 | created_at: now, | ||
| 60 | updated_at: now, | ||
| 61 | }; | ||
| 62 | |||
| 63 | let sql = "INSERT INTO users \ | ||
| 64 | (id, username, username_lower, email, email_lower, display_name, \ | ||
| 65 | password_hash, must_change_password, is_admin, disabled_at, created_at, updated_at) \ | ||
| 66 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"; | ||
| 67 | sqlx::query(sql) | ||
| 68 | .bind(user.id.clone()) | ||
| 69 | .bind(user.username.clone()) | ||
| 70 | .bind(user.username.to_lowercase()) | ||
| 71 | .bind(user.email.clone()) | ||
| 72 | .bind(user.email.to_lowercase()) | ||
| 73 | .bind(user.display_name.clone()) | ||
| 74 | .bind(user.password_hash.clone()) | ||
| 75 | .bind(user.must_change_password) | ||
| 76 | .bind(user.is_admin) | ||
| 77 | .bind(user.disabled_at) | ||
| 78 | .bind(user.created_at) | ||
| 79 | .bind(user.updated_at) | ||
| 80 | .execute(&self.pool) | ||
| 81 | .await | ||
| 82 | .map_err(|e| { | ||
| 83 | map_conflict(e, "user", &[("email", "email"), ("username", "username")]) | ||
| 84 | })?; | ||
| 85 | |||
| 86 | Ok(user) | ||
| 87 | } | ||
| 88 | |||
| 89 | /// Fetch a user by id, or `None` if there is no such user. | ||
| 90 | /// | ||
| 91 | /// # Errors | ||
| 92 | /// | ||
| 93 | /// Returns [`StoreError::Query`] on a database failure. | ||
| 94 | pub async fn user_by_id(&self, id: &str) -> Result<Option<User>, StoreError> { | ||
| 95 | let sql = format!("SELECT {USER_COLUMNS} FROM users WHERE id = $1"); | ||
| 96 | let row = sqlx::query(AssertSqlSafe(sql)) | ||
| 97 | .bind(id) | ||
| 98 | .fetch_optional(&self.pool) | ||
| 99 | .await?; | ||
| 100 | Ok(row.as_ref().map(|r| self.map_user(r)).transpose()?) | ||
| 101 | } | ||
| 102 | |||
| 103 | /// Fetch a user by username, case-insensitively, or `None` if absent. | ||
| 104 | /// | ||
| 105 | /// # Errors | ||
| 106 | /// | ||
| 107 | /// Returns [`StoreError::Query`] on a database failure. | ||
| 108 | pub async fn user_by_username(&self, username: &str) -> Result<Option<User>, StoreError> { | ||
| 109 | let sql = format!("SELECT {USER_COLUMNS} FROM users WHERE username_lower = $1"); | ||
| 110 | let row = sqlx::query(AssertSqlSafe(sql)) | ||
| 111 | .bind(username.to_lowercase()) | ||
| 112 | .fetch_optional(&self.pool) | ||
| 113 | .await?; | ||
| 114 | Ok(row.as_ref().map(|r| self.map_user(r)).transpose()?) | ||
| 115 | } | ||
| 116 | |||
| 117 | /// Map a `users` row (selected via [`USER_COLUMNS`]) to a [`User`]. | ||
| 118 | fn map_user(&self, row: &AnyRow) -> Result<User, sqlx::Error> { | ||
| 119 | Ok(User { | ||
| 120 | id: row.try_get("id")?, | ||
| 121 | username: row.try_get("username")?, | ||
| 122 | email: row.try_get("email")?, | ||
| 123 | display_name: row.try_get("display_name")?, | ||
| 124 | password_hash: row.try_get("password_hash")?, | ||
| 125 | must_change_password: self.get_bool(row, "must_change_password")?, | ||
| 126 | is_admin: self.get_bool(row, "is_admin")?, | ||
| 127 | disabled_at: row.try_get("disabled_at")?, | ||
| 128 | created_at: row.try_get("created_at")?, | ||
| 129 | updated_at: row.try_get("updated_at")?, | ||
| 130 | }) | ||
| 131 | } | ||
| 132 | } | ||
flake.nix +12 −1
| @@ -34,8 +34,19 @@ | |||
| 34 | 34 | ||
| 35 | craneLib = (inputs.crane.mkLib pkgs).overrideToolchain fenixToolchain; | 35 | craneLib = (inputs.crane.mkLib pkgs).overrideToolchain fenixToolchain; |
| 36 | 36 | ||
| 37 | # Keep `.sql` migration files in the build source: the `store` crate | ||
| 38 | # embeds them via `sqlx::migrate!` (and reads them in tests), and | ||
| 39 | # crane's default cargo-source filter would strip them. | ||
| 40 | src = pkgs.lib.cleanSourceWith { | ||
| 41 | src = ./.; | ||
| 42 | name = "source"; | ||
| 43 | filter = | ||
| 44 | path: type: | ||
| 45 | (builtins.match ".*\\.sql$" path != null) || (craneLib.filterCargoSources path type); | ||
| 46 | }; | ||
| 47 | |||
| 37 | commonArgs = { | 48 | commonArgs = { |
| 38 | src = craneLib.cleanCargoSource ./.; | 49 | inherit src; |
| 39 | strictDeps = true; | 50 | strictDeps = true; |
| 40 | nativeBuildInputs = [ | 51 | nativeBuildInputs = [ |
| 41 | pkgs.pkg-config | 52 | pkgs.pkg-config |
migrations/postgres/0001_init.sql +141 −0
| @@ -0,0 +1,141 @@ | |||
| 1 | -- This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | -- License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | -- file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | -- Initial schema (PostgreSQL dialect). | ||
| 6 | -- | ||
| 7 | -- This must stay structurally equivalent to migrations/sqlite/0001_init.sql; | ||
| 8 | -- the only permitted differences are the boolean column type (BOOLEAN here, | ||
| 9 | -- INTEGER 0/1 there) and dialect punctuation. Timestamps are BIGINT Unix | ||
| 10 | -- milliseconds UTC; identifiers are 26-char ULID TEXT (lexicographically | ||
| 11 | -- sortable — order by them). Case-insensitive uniqueness is enforced with a | ||
| 12 | -- normalized `*_lower` column plus a unique index, never collation. | ||
| 13 | |||
| 14 | CREATE TABLE users ( | ||
| 15 | id TEXT PRIMARY KEY, | ||
| 16 | username TEXT NOT NULL, | ||
| 17 | username_lower TEXT NOT NULL UNIQUE, | ||
| 18 | email TEXT NOT NULL, | ||
| 19 | email_lower TEXT NOT NULL UNIQUE, | ||
| 20 | display_name TEXT, | ||
| 21 | password_hash TEXT, -- NULL until an invite is completed | ||
| 22 | must_change_password BOOLEAN NOT NULL DEFAULT FALSE, | ||
| 23 | is_admin BOOLEAN NOT NULL DEFAULT FALSE, | ||
| 24 | disabled_at BIGINT, | ||
| 25 | created_at BIGINT NOT NULL, | ||
| 26 | updated_at BIGINT NOT NULL | ||
| 27 | ); | ||
| 28 | |||
| 29 | CREATE TABLE invites ( | ||
| 30 | id TEXT PRIMARY KEY, | ||
| 31 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 32 | token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of the emailed token | ||
| 33 | purpose TEXT NOT NULL, -- 'activate' | 'reset' | ||
| 34 | expires_at BIGINT NOT NULL, | ||
| 35 | used_at BIGINT, | ||
| 36 | created_at BIGINT NOT NULL | ||
| 37 | ); | ||
| 38 | |||
| 39 | CREATE TABLE sessions ( | ||
| 40 | id TEXT PRIMARY KEY, -- SHA-256 of the cookie value | ||
| 41 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 42 | user_agent TEXT, | ||
| 43 | ip TEXT, | ||
| 44 | created_at BIGINT NOT NULL, | ||
| 45 | last_seen_at BIGINT NOT NULL, | ||
| 46 | expires_at BIGINT NOT NULL | ||
| 47 | ); | ||
| 48 | |||
| 49 | CREATE TABLE api_tokens ( | ||
| 50 | id TEXT PRIMARY KEY, -- the JWT `jti` | ||
| 51 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 52 | name TEXT NOT NULL, | ||
| 53 | scopes TEXT NOT NULL, -- comma-separated | ||
| 54 | expires_at BIGINT, | ||
| 55 | revoked_at BIGINT, | ||
| 56 | last_used_at BIGINT, | ||
| 57 | created_at BIGINT NOT NULL | ||
| 58 | ); | ||
| 59 | |||
| 60 | CREATE TABLE keys ( | ||
| 61 | id TEXT PRIMARY KEY, | ||
| 62 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 63 | kind TEXT NOT NULL, -- 'ssh' | 'gpg' | ||
| 64 | name TEXT, | ||
| 65 | fingerprint TEXT NOT NULL, -- SHA256:… for ssh, full hex fp for gpg | ||
| 66 | public_key TEXT NOT NULL, -- authorized_keys line or armored pgp block | ||
| 67 | ordinal INTEGER NOT NULL, -- stable 1-based index per (user, kind) | ||
| 68 | last_used_at BIGINT, | ||
| 69 | created_at BIGINT NOT NULL, | ||
| 70 | UNIQUE (kind, fingerprint), | ||
| 71 | UNIQUE (user_id, kind, ordinal) | ||
| 72 | ); | ||
| 73 | |||
| 74 | CREATE TABLE groups ( | ||
| 75 | id TEXT PRIMARY KEY, | ||
| 76 | owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 77 | parent_id TEXT REFERENCES groups(id) ON DELETE CASCADE, | ||
| 78 | name TEXT NOT NULL, | ||
| 79 | path TEXT NOT NULL, -- materialized 'group/sub/leaf' | ||
| 80 | description TEXT, | ||
| 81 | created_at BIGINT NOT NULL, | ||
| 82 | UNIQUE (owner_id, path) | ||
| 83 | ); | ||
| 84 | |||
| 85 | CREATE TABLE repos ( | ||
| 86 | id TEXT PRIMARY KEY, | ||
| 87 | owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 88 | group_id TEXT REFERENCES groups(id) ON DELETE SET NULL, | ||
| 89 | name TEXT NOT NULL, | ||
| 90 | path TEXT NOT NULL, -- materialized 'group/sub/repo' (== name when ungrouped) | ||
| 91 | description TEXT, | ||
| 92 | is_private BOOLEAN NOT NULL DEFAULT TRUE, | ||
| 93 | default_branch TEXT NOT NULL, | ||
| 94 | archived_at BIGINT, | ||
| 95 | size_bytes BIGINT NOT NULL DEFAULT 0, | ||
| 96 | pushed_at BIGINT, | ||
| 97 | created_at BIGINT NOT NULL, | ||
| 98 | updated_at BIGINT NOT NULL, | ||
| 99 | UNIQUE (owner_id, path) | ||
| 100 | ); | ||
| 101 | |||
| 102 | CREATE TABLE repo_collaborators ( | ||
| 103 | repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE, | ||
| 104 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 105 | permission TEXT NOT NULL, -- 'read' | 'write' | 'admin' | ||
| 106 | created_at BIGINT NOT NULL, | ||
| 107 | PRIMARY KEY (repo_id, user_id) | ||
| 108 | ); | ||
| 109 | |||
| 110 | -- Reserved and inert until the CI ships (phase 18+). No behaviour reads these | ||
| 111 | -- yet; they exist so the seams are stable. | ||
| 112 | CREATE TABLE runs ( | ||
| 113 | id TEXT PRIMARY KEY, | ||
| 114 | repo_id TEXT NOT NULL, | ||
| 115 | ref TEXT NOT NULL, | ||
| 116 | commit_sha TEXT NOT NULL, | ||
| 117 | status TEXT NOT NULL, | ||
| 118 | started_at BIGINT, | ||
| 119 | finished_at BIGINT, | ||
| 120 | created_at BIGINT NOT NULL | ||
| 121 | ); | ||
| 122 | |||
| 123 | CREATE TABLE jobs ( | ||
| 124 | id TEXT PRIMARY KEY, | ||
| 125 | run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, | ||
| 126 | stage TEXT NOT NULL, | ||
| 127 | name TEXT NOT NULL, | ||
| 128 | status TEXT NOT NULL, | ||
| 129 | log_path TEXT, | ||
| 130 | started_at BIGINT, | ||
| 131 | finished_at BIGINT | ||
| 132 | ); | ||
| 133 | |||
| 134 | -- Indexes. The UNIQUE(owner_id, path) constraint on repos already provides the | ||
| 135 | -- (owner_id, path) lookup index, so it is not repeated here. | ||
| 136 | CREATE INDEX idx_repos_visibility_pushed ON repos (is_private, pushed_at DESC); | ||
| 137 | CREATE INDEX idx_groups_owner_parent ON groups (owner_id, parent_id); | ||
| 138 | CREATE INDEX idx_keys_user_kind ON keys (user_id, kind); | ||
| 139 | CREATE INDEX idx_sessions_user ON sessions (user_id); | ||
| 140 | CREATE INDEX idx_sessions_expires ON sessions (expires_at); | ||
| 141 | CREATE INDEX idx_api_tokens_user ON api_tokens (user_id); | ||
migrations/sqlite/0001_init.sql +141 −0
| @@ -0,0 +1,141 @@ | |||
| 1 | -- This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | -- License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | -- file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | -- Initial schema (SQLite dialect). | ||
| 6 | -- | ||
| 7 | -- This must stay structurally equivalent to migrations/postgres/0001_init.sql; | ||
| 8 | -- the only permitted differences are the boolean column type (INTEGER 0/1 here, | ||
| 9 | -- BOOLEAN there) and dialect punctuation. Timestamps are BIGINT Unix | ||
| 10 | -- milliseconds UTC; identifiers are 26-char ULID TEXT (lexicographically | ||
| 11 | -- sortable — order by them). Case-insensitive uniqueness is enforced with a | ||
| 12 | -- normalized `*_lower` column plus a unique index, never collation. | ||
| 13 | |||
| 14 | CREATE TABLE users ( | ||
| 15 | id TEXT PRIMARY KEY, | ||
| 16 | username TEXT NOT NULL, | ||
| 17 | username_lower TEXT NOT NULL UNIQUE, | ||
| 18 | email TEXT NOT NULL, | ||
| 19 | email_lower TEXT NOT NULL UNIQUE, | ||
| 20 | display_name TEXT, | ||
| 21 | password_hash TEXT, -- NULL until an invite is completed | ||
| 22 | must_change_password INTEGER NOT NULL DEFAULT 0, | ||
| 23 | is_admin INTEGER NOT NULL DEFAULT 0, | ||
| 24 | disabled_at BIGINT, | ||
| 25 | created_at BIGINT NOT NULL, | ||
| 26 | updated_at BIGINT NOT NULL | ||
| 27 | ); | ||
| 28 | |||
| 29 | CREATE TABLE invites ( | ||
| 30 | id TEXT PRIMARY KEY, | ||
| 31 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 32 | token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of the emailed token | ||
| 33 | purpose TEXT NOT NULL, -- 'activate' | 'reset' | ||
| 34 | expires_at BIGINT NOT NULL, | ||
| 35 | used_at BIGINT, | ||
| 36 | created_at BIGINT NOT NULL | ||
| 37 | ); | ||
| 38 | |||
| 39 | CREATE TABLE sessions ( | ||
| 40 | id TEXT PRIMARY KEY, -- SHA-256 of the cookie value | ||
| 41 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 42 | user_agent TEXT, | ||
| 43 | ip TEXT, | ||
| 44 | created_at BIGINT NOT NULL, | ||
| 45 | last_seen_at BIGINT NOT NULL, | ||
| 46 | expires_at BIGINT NOT NULL | ||
| 47 | ); | ||
| 48 | |||
| 49 | CREATE TABLE api_tokens ( | ||
| 50 | id TEXT PRIMARY KEY, -- the JWT `jti` | ||
| 51 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 52 | name TEXT NOT NULL, | ||
| 53 | scopes TEXT NOT NULL, -- comma-separated | ||
| 54 | expires_at BIGINT, | ||
| 55 | revoked_at BIGINT, | ||
| 56 | last_used_at BIGINT, | ||
| 57 | created_at BIGINT NOT NULL | ||
| 58 | ); | ||
| 59 | |||
| 60 | CREATE TABLE keys ( | ||
| 61 | id TEXT PRIMARY KEY, | ||
| 62 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 63 | kind TEXT NOT NULL, -- 'ssh' | 'gpg' | ||
| 64 | name TEXT, | ||
| 65 | fingerprint TEXT NOT NULL, -- SHA256:… for ssh, full hex fp for gpg | ||
| 66 | public_key TEXT NOT NULL, -- authorized_keys line or armored pgp block | ||
| 67 | ordinal INTEGER NOT NULL, -- stable 1-based index per (user, kind) | ||
| 68 | last_used_at BIGINT, | ||
| 69 | created_at BIGINT NOT NULL, | ||
| 70 | UNIQUE (kind, fingerprint), | ||
| 71 | UNIQUE (user_id, kind, ordinal) | ||
| 72 | ); | ||
| 73 | |||
| 74 | CREATE TABLE groups ( | ||
| 75 | id TEXT PRIMARY KEY, | ||
| 76 | owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 77 | parent_id TEXT REFERENCES groups(id) ON DELETE CASCADE, | ||
| 78 | name TEXT NOT NULL, | ||
| 79 | path TEXT NOT NULL, -- materialized 'group/sub/leaf' | ||
| 80 | description TEXT, | ||
| 81 | created_at BIGINT NOT NULL, | ||
| 82 | UNIQUE (owner_id, path) | ||
| 83 | ); | ||
| 84 | |||
| 85 | CREATE TABLE repos ( | ||
| 86 | id TEXT PRIMARY KEY, | ||
| 87 | owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 88 | group_id TEXT REFERENCES groups(id) ON DELETE SET NULL, | ||
| 89 | name TEXT NOT NULL, | ||
| 90 | path TEXT NOT NULL, -- materialized 'group/sub/repo' (== name when ungrouped) | ||
| 91 | description TEXT, | ||
| 92 | is_private INTEGER NOT NULL DEFAULT 1, | ||
| 93 | default_branch TEXT NOT NULL, | ||
| 94 | archived_at BIGINT, | ||
| 95 | size_bytes BIGINT NOT NULL DEFAULT 0, | ||
| 96 | pushed_at BIGINT, | ||
| 97 | created_at BIGINT NOT NULL, | ||
| 98 | updated_at BIGINT NOT NULL, | ||
| 99 | UNIQUE (owner_id, path) | ||
| 100 | ); | ||
| 101 | |||
| 102 | CREATE TABLE repo_collaborators ( | ||
| 103 | repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE, | ||
| 104 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 105 | permission TEXT NOT NULL, -- 'read' | 'write' | 'admin' | ||
| 106 | created_at BIGINT NOT NULL, | ||
| 107 | PRIMARY KEY (repo_id, user_id) | ||
| 108 | ); | ||
| 109 | |||
| 110 | -- Reserved and inert until the CI ships (phase 18+). No behaviour reads these | ||
| 111 | -- yet; they exist so the seams are stable. | ||
| 112 | CREATE TABLE runs ( | ||
| 113 | id TEXT PRIMARY KEY, | ||
| 114 | repo_id TEXT NOT NULL, | ||
| 115 | ref TEXT NOT NULL, | ||
| 116 | commit_sha TEXT NOT NULL, | ||
| 117 | status TEXT NOT NULL, | ||
| 118 | started_at BIGINT, | ||
| 119 | finished_at BIGINT, | ||
| 120 | created_at BIGINT NOT NULL | ||
| 121 | ); | ||
| 122 | |||
| 123 | CREATE TABLE jobs ( | ||
| 124 | id TEXT PRIMARY KEY, | ||
| 125 | run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, | ||
| 126 | stage TEXT NOT NULL, | ||
| 127 | name TEXT NOT NULL, | ||
| 128 | status TEXT NOT NULL, | ||
| 129 | log_path TEXT, | ||
| 130 | started_at BIGINT, | ||
| 131 | finished_at BIGINT | ||
| 132 | ); | ||
| 133 | |||
| 134 | -- Indexes. The UNIQUE(owner_id, path) constraint on repos already provides the | ||
| 135 | -- (owner_id, path) lookup index, so it is not repeated here. | ||
| 136 | CREATE INDEX idx_repos_visibility_pushed ON repos (is_private, pushed_at DESC); | ||
| 137 | CREATE INDEX idx_groups_owner_parent ON groups (owner_id, parent_id); | ||
| 138 | CREATE INDEX idx_keys_user_kind ON keys (user_id, kind); | ||
| 139 | CREATE INDEX idx_sessions_user ON sessions (user_id); | ||
| 140 | CREATE INDEX idx_sessions_expires ON sessions (expires_at); | ||
| 141 | CREATE INDEX idx_api_tokens_user ON api_tokens (user_id); | ||