├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.sh ├── example-app ├── Cargo.toml └── src │ └── lib.rs ├── github-oauth ├── Cargo.toml ├── login.html ├── src │ ├── api.rs │ ├── api │ │ ├── authenticate.rs │ │ ├── authorize.rs │ │ ├── callback.rs │ │ └── login.rs │ └── lib.rs └── wits │ ├── deps │ ├── cli │ │ └── main.wit │ ├── clocks │ │ └── main.wit │ ├── http │ │ └── main.wit │ ├── io │ │ └── main.wit │ └── random │ │ └── main.wit │ └── main.wit └── spin.toml /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # The goal of this Dockerfile is to be architecture independent. To that end, 2 | # it avoids downloading any platform-specific binaries, and installs the required 3 | # tools either through Debian's package manager, or through installation scripts 4 | # that download the appropriate binaries. 5 | 6 | # If the examples in this repository require it, update this Dockerfile to install 7 | # more language toolchains (such as .NET or TinyGo). 8 | 9 | FROM --platform=linux/amd64 ubuntu:22.04 10 | 11 | RUN apt-get update && apt-get install -y \ 12 | bash \ 13 | git \ 14 | curl \ 15 | nodejs \ 16 | npm \ 17 | golang-go \ 18 | build-essential libssl-dev pkg-config\ 19 | glibc-source \ 20 | ca-certificates \ 21 | tree \ 22 | wget 23 | 24 | # Install Rust 25 | RUN curl https://sh.rustup.rs -sSf | bash -s -- -y 26 | ENV PATH="/root/.cargo/bin:${PATH}" 27 | RUN rustup target add wasm32-wasi 28 | 29 | # Install cargo-component 30 | RUN cargo install --git https://github.com/bytecodealliance/cargo-component --locked cargo-component 31 | 32 | # Install wasm-tools 33 | RUN cargo install --git https://github.com/dicej/wasm-tools --branch wasm-compose-resource-imports wasm-tools --locked 34 | 35 | # Install the gopls Go Language Server, see https://github.com/golang/tools/tree/master/gopls 36 | RUN go install golang.org/x/tools/gopls@latest 37 | 38 | # Install Spin and required plugins 39 | RUN (curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash -s -- -v v2.0.0-rc.1 && mv spin /usr/local/bin/) && \ 40 | spin templates install --git https://github.com/radu-matei/spin-kv-explorer --update && \ 41 | spin templates install --git https://github.com/radu-matei/spin-nextjs --update 42 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fermyon Spin", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | // "image": "ghcr.io/fermyon/workshops/dev-container:20230920-075415-ge7406ad", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | "features": { 9 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 10 | "ghcr.io/devcontainers/features/common-utils:2": { 11 | "configureZshAsDefaultShell": true 12 | } 13 | }, 14 | 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined", 19 | "--privileged", 20 | "--init" 21 | ], 22 | 23 | "otherPortsAttributes": { 24 | "onAutoForward": "ignore" 25 | }, 26 | "customizations": { 27 | "vscode": { 28 | "extensions": [ 29 | "mutantdino.resourcemonitor", 30 | "humao.rest-client", 31 | "rust-lang.rust-analyzer", 32 | "tamasfe.even-better-toml", 33 | "golang.Go", 34 | "alexcvzz.vscode-sqlite", 35 | "qwtel.sqlite-viewer", 36 | "bytecodealliance.wit-idl", 37 | "ms-vscode.makefile-tools" 38 | ] 39 | }, 40 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 41 | // "mounts": [ 42 | // { 43 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 44 | // "target": "/usr/local/cargo", 45 | // "type": "volume" 46 | // } 47 | // ] 48 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 49 | "forwardPorts": [ 50 | 3000 51 | ] 52 | // Use 'postCreateCommand' to run commands after the container is created. 53 | // "postCreateCommand": "rustc --version", 54 | // Configure tool-specific properties. 55 | // "customizations": {}, 56 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 57 | // "remoteUser": "root" 58 | } 59 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .spin/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[rust]": { 3 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 4 | }, 5 | "rust-analyzer.linkedProjects": [ 6 | "github-oauth/Cargo.toml", 7 | "example/Cargo.toml" 8 | ], 9 | "rust-analyzer.checkOnSave": true, 10 | "rust-analyzer.check.overrideCommand": ["cargo", "component", "check", "--message-format=json"], 11 | // VS Code don't watch files under ./target 12 | "files.watcherExclude": { 13 | "**/target/**": true 14 | } 15 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.86" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 25 | 26 | [[package]] 27 | name = "async-trait" 28 | version = "0.1.81" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" 31 | dependencies = [ 32 | "proc-macro2", 33 | "quote", 34 | "syn 2.0.76", 35 | ] 36 | 37 | [[package]] 38 | name = "autocfg" 39 | version = "1.3.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 42 | 43 | [[package]] 44 | name = "base64" 45 | version = "0.22.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 48 | 49 | [[package]] 50 | name = "bitflags" 51 | version = "2.6.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 54 | 55 | [[package]] 56 | name = "block-buffer" 57 | version = "0.10.4" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 60 | dependencies = [ 61 | "generic-array", 62 | ] 63 | 64 | [[package]] 65 | name = "bumpalo" 66 | version = "3.16.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 69 | 70 | [[package]] 71 | name = "byteorder" 72 | version = "1.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 75 | 76 | [[package]] 77 | name = "bytes" 78 | version = "1.7.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 81 | 82 | [[package]] 83 | name = "cc" 84 | version = "1.1.15" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" 87 | dependencies = [ 88 | "shlex", 89 | ] 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "chrono" 99 | version = "0.4.38" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 102 | dependencies = [ 103 | "android-tzdata", 104 | "iana-time-zone", 105 | "js-sys", 106 | "num-traits", 107 | "serde", 108 | "wasm-bindgen", 109 | "windows-targets", 110 | ] 111 | 112 | [[package]] 113 | name = "cookie" 114 | version = "0.18.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 117 | dependencies = [ 118 | "time", 119 | "version_check", 120 | ] 121 | 122 | [[package]] 123 | name = "core-foundation-sys" 124 | version = "0.8.7" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 127 | 128 | [[package]] 129 | name = "cpufeatures" 130 | version = "0.2.13" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" 133 | dependencies = [ 134 | "libc", 135 | ] 136 | 137 | [[package]] 138 | name = "crypto-common" 139 | version = "0.1.6" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 142 | dependencies = [ 143 | "generic-array", 144 | "typenum", 145 | ] 146 | 147 | [[package]] 148 | name = "deranged" 149 | version = "0.3.11" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 152 | dependencies = [ 153 | "powerfmt", 154 | ] 155 | 156 | [[package]] 157 | name = "digest" 158 | version = "0.10.7" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 161 | dependencies = [ 162 | "block-buffer", 163 | "crypto-common", 164 | ] 165 | 166 | [[package]] 167 | name = "equivalent" 168 | version = "1.0.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 171 | 172 | [[package]] 173 | name = "example" 174 | version = "0.1.0" 175 | dependencies = [ 176 | "anyhow", 177 | "http", 178 | "spin-sdk", 179 | ] 180 | 181 | [[package]] 182 | name = "fnv" 183 | version = "1.0.7" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 186 | 187 | [[package]] 188 | name = "form_urlencoded" 189 | version = "1.2.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 192 | dependencies = [ 193 | "percent-encoding", 194 | ] 195 | 196 | [[package]] 197 | name = "futures" 198 | version = "0.3.30" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 201 | dependencies = [ 202 | "futures-channel", 203 | "futures-core", 204 | "futures-executor", 205 | "futures-io", 206 | "futures-sink", 207 | "futures-task", 208 | "futures-util", 209 | ] 210 | 211 | [[package]] 212 | name = "futures-channel" 213 | version = "0.3.30" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 216 | dependencies = [ 217 | "futures-core", 218 | "futures-sink", 219 | ] 220 | 221 | [[package]] 222 | name = "futures-core" 223 | version = "0.3.30" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 226 | 227 | [[package]] 228 | name = "futures-executor" 229 | version = "0.3.30" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 232 | dependencies = [ 233 | "futures-core", 234 | "futures-task", 235 | "futures-util", 236 | ] 237 | 238 | [[package]] 239 | name = "futures-io" 240 | version = "0.3.30" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 243 | 244 | [[package]] 245 | name = "futures-macro" 246 | version = "0.3.30" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 249 | dependencies = [ 250 | "proc-macro2", 251 | "quote", 252 | "syn 2.0.76", 253 | ] 254 | 255 | [[package]] 256 | name = "futures-sink" 257 | version = "0.3.30" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 260 | 261 | [[package]] 262 | name = "futures-task" 263 | version = "0.3.30" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 266 | 267 | [[package]] 268 | name = "futures-util" 269 | version = "0.3.30" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 272 | dependencies = [ 273 | "futures-channel", 274 | "futures-core", 275 | "futures-io", 276 | "futures-macro", 277 | "futures-sink", 278 | "futures-task", 279 | "memchr", 280 | "pin-project-lite", 281 | "pin-utils", 282 | "slab", 283 | ] 284 | 285 | [[package]] 286 | name = "generic-array" 287 | version = "0.14.7" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 290 | dependencies = [ 291 | "typenum", 292 | "version_check", 293 | ] 294 | 295 | [[package]] 296 | name = "getrandom" 297 | version = "0.2.15" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 300 | dependencies = [ 301 | "cfg-if", 302 | "js-sys", 303 | "libc", 304 | "wasi", 305 | "wasm-bindgen", 306 | ] 307 | 308 | [[package]] 309 | name = "github-oauth" 310 | version = "0.1.0" 311 | dependencies = [ 312 | "anyhow", 313 | "cookie", 314 | "futures", 315 | "http", 316 | "json", 317 | "oauth2", 318 | "serde", 319 | "serde_json", 320 | "spin-executor", 321 | "spin-sdk", 322 | "url", 323 | "wit-bindgen", 324 | ] 325 | 326 | [[package]] 327 | name = "hashbrown" 328 | version = "0.14.5" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 331 | 332 | [[package]] 333 | name = "heck" 334 | version = "0.4.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 337 | dependencies = [ 338 | "unicode-segmentation", 339 | ] 340 | 341 | [[package]] 342 | name = "http" 343 | version = "1.1.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 346 | dependencies = [ 347 | "bytes", 348 | "fnv", 349 | "itoa", 350 | ] 351 | 352 | [[package]] 353 | name = "iana-time-zone" 354 | version = "0.1.60" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 357 | dependencies = [ 358 | "android_system_properties", 359 | "core-foundation-sys", 360 | "iana-time-zone-haiku", 361 | "js-sys", 362 | "wasm-bindgen", 363 | "windows-core", 364 | ] 365 | 366 | [[package]] 367 | name = "iana-time-zone-haiku" 368 | version = "0.1.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 371 | dependencies = [ 372 | "cc", 373 | ] 374 | 375 | [[package]] 376 | name = "id-arena" 377 | version = "2.2.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" 380 | 381 | [[package]] 382 | name = "idna" 383 | version = "0.5.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 386 | dependencies = [ 387 | "unicode-bidi", 388 | "unicode-normalization", 389 | ] 390 | 391 | [[package]] 392 | name = "indexmap" 393 | version = "2.4.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" 396 | dependencies = [ 397 | "equivalent", 398 | "hashbrown", 399 | "serde", 400 | ] 401 | 402 | [[package]] 403 | name = "itoa" 404 | version = "1.0.11" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 407 | 408 | [[package]] 409 | name = "js-sys" 410 | version = "0.3.70" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 413 | dependencies = [ 414 | "wasm-bindgen", 415 | ] 416 | 417 | [[package]] 418 | name = "json" 419 | version = "0.12.4" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" 422 | 423 | [[package]] 424 | name = "leb128" 425 | version = "0.2.5" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 428 | 429 | [[package]] 430 | name = "libc" 431 | version = "0.2.158" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 434 | 435 | [[package]] 436 | name = "log" 437 | version = "0.4.22" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 440 | 441 | [[package]] 442 | name = "memchr" 443 | version = "2.7.4" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 446 | 447 | [[package]] 448 | name = "num-conv" 449 | version = "0.1.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 452 | 453 | [[package]] 454 | name = "num-traits" 455 | version = "0.2.19" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 458 | dependencies = [ 459 | "autocfg", 460 | ] 461 | 462 | [[package]] 463 | name = "oauth2" 464 | version = "5.0.0-alpha.4" 465 | source = "git+https://github.com/ramosbugs/oauth2-rs?rev=c74aec9#c74aec9dd16e967adb34d00cb702a69d7a79cb3c" 466 | dependencies = [ 467 | "base64", 468 | "chrono", 469 | "getrandom", 470 | "http", 471 | "rand", 472 | "serde", 473 | "serde_json", 474 | "serde_path_to_error", 475 | "sha2", 476 | "thiserror", 477 | "url", 478 | ] 479 | 480 | [[package]] 481 | name = "once_cell" 482 | version = "1.19.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 485 | 486 | [[package]] 487 | name = "percent-encoding" 488 | version = "2.3.1" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 491 | 492 | [[package]] 493 | name = "pin-project-lite" 494 | version = "0.2.14" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 497 | 498 | [[package]] 499 | name = "pin-utils" 500 | version = "0.1.0" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 503 | 504 | [[package]] 505 | name = "powerfmt" 506 | version = "0.2.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 509 | 510 | [[package]] 511 | name = "ppv-lite86" 512 | version = "0.2.20" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 515 | dependencies = [ 516 | "zerocopy", 517 | ] 518 | 519 | [[package]] 520 | name = "proc-macro2" 521 | version = "1.0.86" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 524 | dependencies = [ 525 | "unicode-ident", 526 | ] 527 | 528 | [[package]] 529 | name = "quote" 530 | version = "1.0.37" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 533 | dependencies = [ 534 | "proc-macro2", 535 | ] 536 | 537 | [[package]] 538 | name = "rand" 539 | version = "0.8.5" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 542 | dependencies = [ 543 | "libc", 544 | "rand_chacha", 545 | "rand_core", 546 | ] 547 | 548 | [[package]] 549 | name = "rand_chacha" 550 | version = "0.3.1" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 553 | dependencies = [ 554 | "ppv-lite86", 555 | "rand_core", 556 | ] 557 | 558 | [[package]] 559 | name = "rand_core" 560 | version = "0.6.4" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 563 | dependencies = [ 564 | "getrandom", 565 | ] 566 | 567 | [[package]] 568 | name = "routefinder" 569 | version = "0.5.4" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" 572 | dependencies = [ 573 | "smartcow", 574 | "smartstring", 575 | ] 576 | 577 | [[package]] 578 | name = "ryu" 579 | version = "1.0.18" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 582 | 583 | [[package]] 584 | name = "semver" 585 | version = "1.0.23" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 588 | 589 | [[package]] 590 | name = "serde" 591 | version = "1.0.209" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 594 | dependencies = [ 595 | "serde_derive", 596 | ] 597 | 598 | [[package]] 599 | name = "serde_derive" 600 | version = "1.0.209" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 603 | dependencies = [ 604 | "proc-macro2", 605 | "quote", 606 | "syn 2.0.76", 607 | ] 608 | 609 | [[package]] 610 | name = "serde_json" 611 | version = "1.0.127" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" 614 | dependencies = [ 615 | "itoa", 616 | "memchr", 617 | "ryu", 618 | "serde", 619 | ] 620 | 621 | [[package]] 622 | name = "serde_path_to_error" 623 | version = "0.1.16" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 626 | dependencies = [ 627 | "itoa", 628 | "serde", 629 | ] 630 | 631 | [[package]] 632 | name = "sha2" 633 | version = "0.10.8" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 636 | dependencies = [ 637 | "cfg-if", 638 | "cpufeatures", 639 | "digest", 640 | ] 641 | 642 | [[package]] 643 | name = "shlex" 644 | version = "1.3.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 647 | 648 | [[package]] 649 | name = "slab" 650 | version = "0.4.9" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 653 | dependencies = [ 654 | "autocfg", 655 | ] 656 | 657 | [[package]] 658 | name = "smallvec" 659 | version = "1.13.2" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 662 | 663 | [[package]] 664 | name = "smartcow" 665 | version = "0.2.1" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" 668 | dependencies = [ 669 | "smartstring", 670 | ] 671 | 672 | [[package]] 673 | name = "smartstring" 674 | version = "1.0.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" 677 | dependencies = [ 678 | "autocfg", 679 | "static_assertions", 680 | "version_check", 681 | ] 682 | 683 | [[package]] 684 | name = "spdx" 685 | version = "0.10.6" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" 688 | dependencies = [ 689 | "smallvec", 690 | ] 691 | 692 | [[package]] 693 | name = "spin-executor" 694 | version = "3.0.1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "2df1a5e2cc70a628c9ea6914770c234cc4a292218091e6707ae8be68b4a5de76" 697 | dependencies = [ 698 | "futures", 699 | "once_cell", 700 | "wit-bindgen", 701 | ] 702 | 703 | [[package]] 704 | name = "spin-macro" 705 | version = "3.0.1" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "ef3d03e5a205a641d85ace3af1604b39dba63d3ffe3865a71bda02fb482ae60a" 708 | dependencies = [ 709 | "anyhow", 710 | "bytes", 711 | "proc-macro2", 712 | "quote", 713 | "syn 1.0.109", 714 | ] 715 | 716 | [[package]] 717 | name = "spin-sdk" 718 | version = "3.0.1" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "ed97f54a15f2d8b1fa15e436d88bacb95a5b379a3e0f8fbd8042eb8696ca048a" 721 | dependencies = [ 722 | "anyhow", 723 | "async-trait", 724 | "bytes", 725 | "form_urlencoded", 726 | "futures", 727 | "http", 728 | "once_cell", 729 | "routefinder", 730 | "serde", 731 | "serde_json", 732 | "spin-executor", 733 | "spin-macro", 734 | "thiserror", 735 | "wit-bindgen", 736 | ] 737 | 738 | [[package]] 739 | name = "static_assertions" 740 | version = "1.1.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 743 | 744 | [[package]] 745 | name = "syn" 746 | version = "1.0.109" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 749 | dependencies = [ 750 | "proc-macro2", 751 | "quote", 752 | "unicode-ident", 753 | ] 754 | 755 | [[package]] 756 | name = "syn" 757 | version = "2.0.76" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" 760 | dependencies = [ 761 | "proc-macro2", 762 | "quote", 763 | "unicode-ident", 764 | ] 765 | 766 | [[package]] 767 | name = "thiserror" 768 | version = "1.0.63" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 771 | dependencies = [ 772 | "thiserror-impl", 773 | ] 774 | 775 | [[package]] 776 | name = "thiserror-impl" 777 | version = "1.0.63" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn 2.0.76", 784 | ] 785 | 786 | [[package]] 787 | name = "time" 788 | version = "0.3.36" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 791 | dependencies = [ 792 | "deranged", 793 | "itoa", 794 | "num-conv", 795 | "powerfmt", 796 | "serde", 797 | "time-core", 798 | "time-macros", 799 | ] 800 | 801 | [[package]] 802 | name = "time-core" 803 | version = "0.1.2" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 806 | 807 | [[package]] 808 | name = "time-macros" 809 | version = "0.2.18" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 812 | dependencies = [ 813 | "num-conv", 814 | "time-core", 815 | ] 816 | 817 | [[package]] 818 | name = "tinyvec" 819 | version = "1.8.0" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 822 | dependencies = [ 823 | "tinyvec_macros", 824 | ] 825 | 826 | [[package]] 827 | name = "tinyvec_macros" 828 | version = "0.1.1" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 831 | 832 | [[package]] 833 | name = "typenum" 834 | version = "1.17.0" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 837 | 838 | [[package]] 839 | name = "unicode-bidi" 840 | version = "0.3.15" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 843 | 844 | [[package]] 845 | name = "unicode-ident" 846 | version = "1.0.12" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 849 | 850 | [[package]] 851 | name = "unicode-normalization" 852 | version = "0.1.23" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 855 | dependencies = [ 856 | "tinyvec", 857 | ] 858 | 859 | [[package]] 860 | name = "unicode-segmentation" 861 | version = "1.11.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 864 | 865 | [[package]] 866 | name = "unicode-xid" 867 | version = "0.2.5" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" 870 | 871 | [[package]] 872 | name = "url" 873 | version = "2.5.2" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 876 | dependencies = [ 877 | "form_urlencoded", 878 | "idna", 879 | "percent-encoding", 880 | "serde", 881 | ] 882 | 883 | [[package]] 884 | name = "version_check" 885 | version = "0.9.5" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 888 | 889 | [[package]] 890 | name = "wasi" 891 | version = "0.11.0+wasi-snapshot-preview1" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 894 | 895 | [[package]] 896 | name = "wasm-bindgen" 897 | version = "0.2.93" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 900 | dependencies = [ 901 | "cfg-if", 902 | "once_cell", 903 | "wasm-bindgen-macro", 904 | ] 905 | 906 | [[package]] 907 | name = "wasm-bindgen-backend" 908 | version = "0.2.93" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 911 | dependencies = [ 912 | "bumpalo", 913 | "log", 914 | "once_cell", 915 | "proc-macro2", 916 | "quote", 917 | "syn 2.0.76", 918 | "wasm-bindgen-shared", 919 | ] 920 | 921 | [[package]] 922 | name = "wasm-bindgen-macro" 923 | version = "0.2.93" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 926 | dependencies = [ 927 | "quote", 928 | "wasm-bindgen-macro-support", 929 | ] 930 | 931 | [[package]] 932 | name = "wasm-bindgen-macro-support" 933 | version = "0.2.93" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 936 | dependencies = [ 937 | "proc-macro2", 938 | "quote", 939 | "syn 2.0.76", 940 | "wasm-bindgen-backend", 941 | "wasm-bindgen-shared", 942 | ] 943 | 944 | [[package]] 945 | name = "wasm-bindgen-shared" 946 | version = "0.2.93" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 949 | 950 | [[package]] 951 | name = "wasm-encoder" 952 | version = "0.38.1" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" 955 | dependencies = [ 956 | "leb128", 957 | ] 958 | 959 | [[package]] 960 | name = "wasm-encoder" 961 | version = "0.41.2" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "972f97a5d8318f908dded23594188a90bcd09365986b1163e66d70170e5287ae" 964 | dependencies = [ 965 | "leb128", 966 | ] 967 | 968 | [[package]] 969 | name = "wasm-metadata" 970 | version = "0.10.20" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "18ebaa7bd0f9e7a5e5dd29b9a998acf21c4abed74265524dd7e85934597bfb10" 973 | dependencies = [ 974 | "anyhow", 975 | "indexmap", 976 | "serde", 977 | "serde_derive", 978 | "serde_json", 979 | "spdx", 980 | "wasm-encoder 0.41.2", 981 | "wasmparser 0.121.2", 982 | ] 983 | 984 | [[package]] 985 | name = "wasmparser" 986 | version = "0.118.2" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "77f1154f1ab868e2a01d9834a805faca7bf8b50d041b4ca714d005d0dab1c50c" 989 | dependencies = [ 990 | "indexmap", 991 | "semver", 992 | ] 993 | 994 | [[package]] 995 | name = "wasmparser" 996 | version = "0.121.2" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" 999 | dependencies = [ 1000 | "bitflags", 1001 | "indexmap", 1002 | "semver", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "windows-core" 1007 | version = "0.52.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1010 | dependencies = [ 1011 | "windows-targets", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "windows-targets" 1016 | version = "0.52.6" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1019 | dependencies = [ 1020 | "windows_aarch64_gnullvm", 1021 | "windows_aarch64_msvc", 1022 | "windows_i686_gnu", 1023 | "windows_i686_gnullvm", 1024 | "windows_i686_msvc", 1025 | "windows_x86_64_gnu", 1026 | "windows_x86_64_gnullvm", 1027 | "windows_x86_64_msvc", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "windows_aarch64_gnullvm" 1032 | version = "0.52.6" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1035 | 1036 | [[package]] 1037 | name = "windows_aarch64_msvc" 1038 | version = "0.52.6" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1041 | 1042 | [[package]] 1043 | name = "windows_i686_gnu" 1044 | version = "0.52.6" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1047 | 1048 | [[package]] 1049 | name = "windows_i686_gnullvm" 1050 | version = "0.52.6" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1053 | 1054 | [[package]] 1055 | name = "windows_i686_msvc" 1056 | version = "0.52.6" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1059 | 1060 | [[package]] 1061 | name = "windows_x86_64_gnu" 1062 | version = "0.52.6" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1065 | 1066 | [[package]] 1067 | name = "windows_x86_64_gnullvm" 1068 | version = "0.52.6" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1071 | 1072 | [[package]] 1073 | name = "windows_x86_64_msvc" 1074 | version = "0.52.6" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1077 | 1078 | [[package]] 1079 | name = "wit-bindgen" 1080 | version = "0.16.0" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "b76f1d099678b4f69402a421e888bbe71bf20320c2f3f3565d0e7484dbe5bc20" 1083 | dependencies = [ 1084 | "bitflags", 1085 | "wit-bindgen-rust-macro", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "wit-bindgen-core" 1090 | version = "0.16.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "75d55e1a488af2981fb0edac80d8d20a51ac36897a1bdef4abde33c29c1b6d0d" 1093 | dependencies = [ 1094 | "anyhow", 1095 | "wit-component", 1096 | "wit-parser", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "wit-bindgen-rust" 1101 | version = "0.16.0" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "a01ff9cae7bf5736750d94d91eb8a49f5e3a04aff1d1a3218287d9b2964510f8" 1104 | dependencies = [ 1105 | "anyhow", 1106 | "heck", 1107 | "wasm-metadata", 1108 | "wit-bindgen-core", 1109 | "wit-component", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "wit-bindgen-rust-macro" 1114 | version = "0.16.0" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "804a98e2538393d47aa7da65a7348116d6ff403b426665152b70a168c0146d49" 1117 | dependencies = [ 1118 | "anyhow", 1119 | "proc-macro2", 1120 | "quote", 1121 | "syn 2.0.76", 1122 | "wit-bindgen-core", 1123 | "wit-bindgen-rust", 1124 | "wit-component", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "wit-component" 1129 | version = "0.18.2" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "5b8a35a2a9992898c9d27f1664001860595a4bc99d32dd3599d547412e17d7e2" 1132 | dependencies = [ 1133 | "anyhow", 1134 | "bitflags", 1135 | "indexmap", 1136 | "log", 1137 | "serde", 1138 | "serde_derive", 1139 | "serde_json", 1140 | "wasm-encoder 0.38.1", 1141 | "wasm-metadata", 1142 | "wasmparser 0.118.2", 1143 | "wit-parser", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "wit-parser" 1148 | version = "0.13.2" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" 1151 | dependencies = [ 1152 | "anyhow", 1153 | "id-arena", 1154 | "indexmap", 1155 | "log", 1156 | "semver", 1157 | "serde", 1158 | "serde_derive", 1159 | "serde_json", 1160 | "unicode-xid", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "zerocopy" 1165 | version = "0.7.35" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1168 | dependencies = [ 1169 | "byteorder", 1170 | "zerocopy-derive", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "zerocopy-derive" 1175 | version = "0.7.35" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1178 | dependencies = [ 1179 | "proc-macro2", 1180 | "quote", 1181 | "syn 2.0.76", 1182 | ] 1183 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "github-oauth", 4 | "example-app", 5 | ] 6 | 7 | [workspace.dependencies] 8 | anyhow = "1" 9 | http = "1.0.0" 10 | spin-sdk = "3.0.1" 11 | spin-executor = "3.0.1" 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | opt-level = "s" 16 | debug = false 17 | strip = true 18 | lto = true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-auth-middleware 2 | 3 | This repo is an example of how to compose a middleware component with a business logic component. 4 | 5 | ## Repo structure 6 | 7 | The `github-oauth/` directory contains an API for using GitHub oauth in an application. It consists of 8 | 9 | 1. The `authorize` handler which kicks off the github oauth flow allowing a user to give permissions to a GitHub app 10 | 2. The `callback` handler which GitHub uses as the redirect url in the oauth flow. The callback handler is responsible for taking a code from the URL param and exchanging it for authentication token from GitHub for the user. 11 | 3. The `authenticate` handler which validates a given access token in an incoming request with the GitHub user API. 12 | 4. The `login` handler which returns a login button. 13 | 14 | The `example-app/` directory contains a Spin application which consists of one http handler which returns an HTTP response contains `Hello, Fermyon!` in the body. 15 | 16 | 17 | ## Demo instructions 18 | 19 | ### Pre-requisites 20 | 21 | - Install [cargo component](https://github.com/bytecodealliance/cargo-component): 22 | 23 | ```bash 24 | cargo install cargo-component --locked 25 | ``` 26 | 27 | - Install latest [Spin](https://github.com/fermyon/spin) 28 | 29 | - Create an OAuth App in your [GitHub Developer Settings](https://github.com/settings/developers). Set the callback URL to `http://127.0.0.1:3000/login/callback`. Accept defaults and input dummy values for the rest of the fields. 30 | - Save the Client ID 31 | - Generate a new Client Secret and save that as well 32 | 33 | ### Build the components and run the demo 34 | 35 | > NOTE: The build script `build.sh` in the `Spin.toml` will build both the `example-app` and `github-oauth` projects. 36 | 37 | ```bash 38 | # Build and run the example 39 | spin up --build -e CLIENT_ID= -e CLIENT_SECRET= 40 | 41 | # Open http://127.0.0.1:3000/ in a browser 42 | ``` 43 | 44 | ### Running with Wasmtime 45 | 46 | This component can be universally run by runtimes that support WASI preview 2's [HTTP proxy 47 | world](https://github.com/WebAssembly/wasi-http/blob/main/wit/proxy.wit). For example, it can be 48 | served directly by Wasmtime, the runtime embedded in Spin. First, ensure you have installed the 49 | [Wasmtime CLI](https://github.com/bytecodealliance/wasmtime/releases) with at least version 50 | `v21.0.1`. We will use the `wasmtime serve` subcommand which serves requests to/from a WASI HTTP 51 | component. 52 | 53 | - Install [wac](https://github.com/bytecodealliance/wac): 54 | 55 | ```bash 56 | cargo install wac-cli --locked 57 | ``` 58 | 59 | ```bash 60 | export CLIENT_ID= 61 | export CLIENT_SECRET= 62 | 63 | # Build the example-app and github-oauth component 64 | spin build 65 | 66 | # Compose the example-app with the github-oauth component 67 | wac plug --plug target/wasm32-wasip1/release/example.wasm target/wasm32-wasip1/release/github_oauth.wasm -o service.wasm 68 | 69 | # Serve the component on the expected host and port 70 | wasmtime serve service.wasm -S cli --addr 127.0.0.1:3000 71 | ``` 72 | 73 | ### Configuring the callback URL 74 | 75 | Instead of using the default callback URL of `http://127.0.0.1:3000/login/callback`, you can configure the URL in an environment variable that is resolved at build time. This is useful in the case that the component is not running locally, rather in a hosted environment such as Fermyon Cloud. 76 | 77 | ```sh 78 | export AUTH_CALLBACK_URL=http://my-auth-app.fermyon.app/login/callback 79 | export CLIENT_ID= 80 | export CLIENT_SECRET= 81 | cargo component build --manifest-path github-oauth/Cargo.toml --release --features compile-time-secrets 82 | spin deploy 83 | ``` 84 | 85 | ### Using Runtime Environment Variables 86 | 87 | Not all WebAssembly runtimes fully support exporting the [`wasi:cli/environment`](https://github.com/WebAssembly/wasi-cli/blob/main/wit/environment.wit) interface to components. Spin, however, does support this and can load environment variables into a component's environment. Simply pass the environment variables during a `spin up`: 88 | ```sh 89 | spin up --build -e CLIENT_ID= -e CLIENT_SECRET= 90 | ``` 91 | 92 | To deploy an app to Fermyon Cloud that uses environment variables, you need to [configure them in your `spin.toml`](https://developer.fermyon.com/spin/v2/writing-apps#adding-environment-variables-to-components). Update [the example application manifest](./example/spin.toml) to contain your `CLIENT_ID` and `CLIENT_SECRET` environment variables. Since we do not know the endpoint for our Fermyon Cloud application until after the first deploy, we cannot yet configure the `AUTH_CALLBACK_URL`. 93 | 94 | ```toml 95 | [component.frontend] 96 | # ... 97 | environment = { CLIENT_ID = "YOUR_GITHUB_APP_CLIENT_ID", CLIENT_SECRET = "YOUR_GITHUB_APP_CLIENT_SECRET" } 98 | ``` 99 | 100 | Now deploy your application. 101 | 102 | ```sh 103 | $ spin deploy 104 | Uploading github-oauth2-example version 0.1.0 to Fermyon Cloud... 105 | Deploying... 106 | Waiting for application to become ready............. ready 107 | Available Routes: 108 | example: https://github-oauth2-example-12345.fermyon.app (wildcard) 109 | ``` 110 | 111 | In the example deploy output above, the app now exists at endpoint `https://github-oauth2-example-12345.fermyon.app`. This means our callback URL should be `https://github-oauth2-example-12345.fermyon.app/login/callback`. Configure this in the `spin.toml` with another environment variable: 112 | 113 | ```toml 114 | [component.frontend] 115 | # ... 116 | environment = { CLIENT_ID = "YOUR_GITHUB_APP_CLIENT_ID", CLIENT_SECRET = "YOUR_GITHUB_APP_CLIENT_SECRET", AUTH_CALLBACK_URL = "https://github-oauth2-example-.fermyon.app/login/callback" } 117 | ``` 118 | 119 | Now, redeploy with another `spin deploy`. Be sure to update your GitHub OAuth App to update the callback URL. 120 | 121 | This example uses environment variable to import secrets, since that is a ubiquitous interface and enables cross cloud portability of your component. If you are interested in configuring dynamic secrets that are not exposed in text in your `spin.toml` and can be updated with the `spin cloud variables` CLI, see [Spin's documentation on configuring application variables](https://developer.fermyon.com/spin/v2/variables#application-variables). -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | cargo component build --release --manifest-path github-oauth/Cargo.toml 2 | cargo component build --release --manifest-path example-app/Cargo.toml -------------------------------------------------------------------------------- /example-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | description = "A simple HTTP handler" 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = [ "cdylib" ] 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | http = { workspace = true } 13 | spin-sdk = { workspace = true } -------------------------------------------------------------------------------- /example-app/src/lib.rs: -------------------------------------------------------------------------------- 1 | use spin_sdk::http::{IntoResponse, Request}; 2 | use spin_sdk::http_component; 3 | 4 | /// A simple Spin HTTP component. 5 | #[http_component] 6 | fn handle_http_handler(_req: Request) -> anyhow::Result { 7 | Ok(http::Response::builder() 8 | .status(200) 9 | .header("content-type", "text/plain") 10 | .body("Business logic executed!")?) 11 | } 12 | -------------------------------------------------------------------------------- /github-oauth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "github-oauth" 3 | description = "An HTTP authentication middleware component." 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | cookie = "0.18" 13 | futures = "0.3.28" 14 | http = "1.0" 15 | json = "0.12.4" 16 | oauth2 = { git = "https://github.com/ramosbugs/oauth2-rs", rev = "c74aec9", default-features = false } 17 | serde = { version = "1.0.190", features = ["derive"] } 18 | serde_json = "1.0.108" 19 | spin-sdk = { workspace = true } 20 | spin-executor = { workspace = true } 21 | url = "2.4.0" 22 | wit-bindgen = "0.16.0" 23 | 24 | [features] 25 | # Inject oauth credentials environment variables at compile time rather than runtime 26 | compile-time-secrets = [] -------------------------------------------------------------------------------- /github-oauth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OAuth Middleware Example 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |

15 | 17 | Login with GitHub 18 | 19 |

20 |

21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /github-oauth/src/api.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; 3 | 4 | pub use authenticate::authenticate; 5 | pub use authorize::authorize; 6 | pub use callback::callback; 7 | pub use login::login; 8 | 9 | mod authenticate; 10 | mod authorize; 11 | mod callback; 12 | mod login; 13 | 14 | pub struct OAuth2 { 15 | pub client_secret: ClientSecret, 16 | pub client_id: ClientId, 17 | pub auth_url: AuthUrl, 18 | pub token_url: TokenUrl, 19 | pub redirect_url: RedirectUrl, 20 | } 21 | 22 | const AUTH_CALLBACK_URL: Option<&'static str> = option_env!("AUTH_CALLBACK_URL"); 23 | 24 | impl OAuth2 { 25 | pub fn try_init() -> anyhow::Result { 26 | let client_secret_env = option_env!("CLIENT_SECRET"); 27 | let client_id_env = option_env!("CLIENT_ID"); 28 | 29 | let (client_secret, client_id) = if !cfg!(feature = "compile-time-secrets") { 30 | (std::env::var("CLIENT_SECRET")?, std::env::var("CLIENT_ID")?) 31 | } else { 32 | ( 33 | client_secret_env 34 | .context("CLIENT_SECRET was not configured at build time")? 35 | .to_string(), 36 | client_id_env 37 | .context("CLIENT_ID was not configured at build time")? 38 | .to_string(), 39 | ) 40 | }; 41 | 42 | let client_secret = ClientSecret::new(client_secret); 43 | let client_id = ClientId::new(client_id); 44 | let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string())?; 45 | let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string())?; 46 | 47 | let redirect_url = match std::env::var("AUTH_CALLBACK_URL") { 48 | Ok(runtime_env) => RedirectUrl::new(runtime_env)?, 49 | Err(_) => RedirectUrl::new( 50 | AUTH_CALLBACK_URL 51 | .unwrap_or("http://127.0.0.1:3000/login/callback") 52 | .to_string(), 53 | )?, 54 | }; 55 | 56 | Ok(OAuth2 { 57 | client_secret, 58 | client_id, 59 | token_url, 60 | auth_url, 61 | redirect_url, 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /github-oauth/src/api/authenticate.rs: -------------------------------------------------------------------------------- 1 | use cookie::Cookie; 2 | use futures::SinkExt; 3 | use http::header::COOKIE; 4 | use spin_sdk::http::{send, Headers, IncomingRequest, OutgoingResponse, ResponseOutparam}; 5 | 6 | /// `authenticate` validates the access token required in the incoming request by making an 7 | /// outgoing request to github. If the token is valid, the request is passed through to the 8 | /// imported endpoint. 9 | pub async fn authenticate(request: IncomingRequest, output: ResponseOutparam) { 10 | let token = match get_access_token(&request) { 11 | Some(token) => token, 12 | None => { 13 | eprintln!("no access token found in incoming request"); 14 | 15 | let headers = Headers::new(); 16 | headers 17 | .set(&"Content-Type".to_string(), &[b"text/html".to_vec()]) 18 | .unwrap(); 19 | 20 | let response = OutgoingResponse::new(headers); 21 | response.set_status_code(403).unwrap(); 22 | 23 | let mut body = response.take_body(); 24 | output.set(response); 25 | 26 | if let Err(error) = body 27 | .send(b"Unauthorized, login".to_vec()) 28 | .await 29 | { 30 | eprintln!("error send login page: {error}"); 31 | } 32 | 33 | return; 34 | } 35 | }; 36 | 37 | let result = send::<_, http::Response<()>>( 38 | http::Request::builder() 39 | .method("GET") 40 | .uri("https://api.github.com/user") 41 | .header("Authorization", format!("Bearer {token}")) 42 | .header("User-Agent", "Spin Middleware") 43 | .body(()) 44 | .unwrap(), 45 | ) 46 | .await; 47 | 48 | match result { 49 | Ok(response) => { 50 | let status = response.status(); 51 | if status.is_success() { 52 | eprintln!("authenticated"); 53 | crate::wasi::http::incoming_handler::handle(request, output.into_inner()); 54 | } else { 55 | eprintln!("unauthenticated"); 56 | 57 | let headers = Headers::new(); 58 | headers 59 | .set(&"Content-Type".to_string(), &[b"text/html".to_vec()]) 60 | .unwrap(); 61 | 62 | let response = OutgoingResponse::new(headers); 63 | response.set_status_code(status.as_u16()).unwrap(); 64 | 65 | let mut body = response.take_body(); 66 | output.set(response); 67 | 68 | if let Err(error) = body 69 | .send(b"Unauthorized, login".to_vec()) 70 | .await 71 | { 72 | eprintln!("error sending page: {error}"); 73 | } 74 | } 75 | } 76 | Err(error) => { 77 | eprintln!("error authenticating with github: {error}"); 78 | let response = OutgoingResponse::new(Headers::new()); 79 | response.set_status_code(500).unwrap(); 80 | output.set(response); 81 | } 82 | } 83 | } 84 | 85 | fn get_access_token(request: &IncomingRequest) -> Option { 86 | let cookies: Vec> = request.headers().get(&COOKIE.to_string()); 87 | for encoded in cookies { 88 | let parsed = Cookie::split_parse(String::from_utf8_lossy(&encoded)); 89 | for cookie in parsed { 90 | if let Ok(cookie) = cookie { 91 | const OAUTH_TOKEN: &str = "access-token"; 92 | if matches!(cookie.name(), OAUTH_TOKEN) { 93 | return Some(cookie.value().to_string()); 94 | } 95 | } 96 | } 97 | } 98 | None 99 | } 100 | -------------------------------------------------------------------------------- /github-oauth/src/api/authorize.rs: -------------------------------------------------------------------------------- 1 | use super::OAuth2; 2 | use oauth2::{basic, CsrfToken, Scope}; 3 | use spin_sdk::http::{Headers, OutgoingResponse, ResponseOutparam}; 4 | 5 | /// `authorize` kicks off the oauth flow constructing the authorization url and redirecting the client to github 6 | /// to authorize the application to the user's profile. 7 | pub async fn authorize(output: ResponseOutparam) { 8 | let client = match OAuth2::try_init() { 9 | Ok(config) => basic::BasicClient::new(config.client_id) 10 | .set_client_secret(config.client_secret) 11 | .set_auth_uri(config.auth_url) 12 | .set_token_uri(config.token_url) 13 | .set_redirect_uri(config.redirect_url) 14 | .set_auth_type(oauth2::AuthType::RequestBody), 15 | Err(error) => { 16 | eprintln!("failed to initialize oauth client: {error}"); 17 | let response = OutgoingResponse::new(Headers::new()); 18 | response.set_status_code(500).unwrap(); 19 | output.set(response); 20 | return; 21 | } 22 | }; 23 | 24 | // Generate the authorization URL to which we'll redirect the user. 25 | let (authorize_url, _csrf_state) = client 26 | .authorize_url(CsrfToken::new_random) 27 | // This example is requesting access to the user's email. 28 | .add_scope(Scope::new("user:email".to_string())) 29 | .url(); 30 | 31 | // TODO: cache the csrf token for validation on callback 32 | 33 | let location = authorize_url.to_string().as_bytes().to_vec(); 34 | let headers = Headers::new(); 35 | headers.set(&"Location".to_string(), &[location]).unwrap(); 36 | 37 | let response = OutgoingResponse::new(headers); 38 | response.set_status_code(301).unwrap(); 39 | output.set(response); 40 | } 41 | -------------------------------------------------------------------------------- /github-oauth/src/api/callback.rs: -------------------------------------------------------------------------------- 1 | use super::OAuth2; 2 | use cookie::{Cookie, SameSite}; 3 | use oauth2::{basic, AuthorizationCode, CsrfToken, TokenResponse}; 4 | use spin_sdk::http::{send, Headers, OutgoingResponse, ResponseOutparam, SendError}; 5 | use url::Url; 6 | 7 | pub async fn callback(url: Url, output: ResponseOutparam) { 8 | let client = match OAuth2::try_init() { 9 | Ok(config) => basic::BasicClient::new(config.client_id) 10 | .set_client_secret(config.client_secret) 11 | .set_auth_uri(config.auth_url) 12 | .set_token_uri(config.token_url) 13 | .set_redirect_uri(config.redirect_url) 14 | .set_auth_type(oauth2::AuthType::RequestBody), 15 | Err(error) => { 16 | eprintln!("failed to initialize oauth client: {error}"); 17 | let response = OutgoingResponse::new(Headers::new()); 18 | response.set_status_code(500).unwrap(); 19 | output.set(response); 20 | return; 21 | } 22 | }; 23 | 24 | let (code, _state) = match get_code_and_state_param(&url) { 25 | Ok((code, state)) => (code, state), 26 | Err(error) => { 27 | eprintln!("error retrieving required query parameters: {error}"); 28 | let response = OutgoingResponse::new(Headers::new()); 29 | response.set_status_code(500).unwrap(); 30 | output.set(response); 31 | return; 32 | } 33 | }; 34 | 35 | // TODO: check state with cached state and ensure equality 36 | 37 | let result = client 38 | .exchange_code(code) 39 | .request_async(&oauth_http_client) 40 | .await; 41 | 42 | let mut location = client.redirect_uri().unwrap().url().clone(); 43 | location.set_path(""); 44 | match result { 45 | Ok(result) => { 46 | let access_token = serde_json::to_string(result.access_token()) 47 | .unwrap() 48 | .replace("\"", ""); 49 | 50 | let mut oauth_cookie = Cookie::new("access-token", access_token); 51 | oauth_cookie.set_same_site(Some(SameSite::Lax)); 52 | oauth_cookie.set_http_only(true); 53 | oauth_cookie.set_path("/"); 54 | 55 | let headers = Headers::new(); 56 | headers 57 | .set(&"Content-Type".to_string(), &[b"text/plain".to_vec()]) 58 | .unwrap(); 59 | headers 60 | .set( 61 | &"Location".to_string(), 62 | &[location.to_string().as_bytes().to_vec()], 63 | ) 64 | .unwrap(); 65 | headers 66 | .set( 67 | &"Set-Cookie".to_string(), 68 | &[oauth_cookie.to_string().as_bytes().to_vec()], 69 | ) 70 | .unwrap(); 71 | 72 | let response = OutgoingResponse::new(headers); 73 | response.set_status_code(301).unwrap(); 74 | output.set(response); 75 | } 76 | Err(error) => { 77 | eprintln!("error exchanging code for token with github: {error}"); 78 | let response = OutgoingResponse::new(Headers::new()); 79 | response.set_status_code(403).unwrap(); 80 | output.set(response); 81 | } 82 | } 83 | } 84 | 85 | fn get_code_and_state_param(url: &Url) -> anyhow::Result<(AuthorizationCode, CsrfToken)> { 86 | fn get_query_param(url: &Url, param: &str) -> Option { 87 | url.query_pairs() 88 | .find(|(key, _)| key == param) 89 | .map(|(_, value)| value.into_owned()) 90 | } 91 | 92 | const STATE_QUERY_PARAM_NAME: &str = "state"; 93 | const CODE_QUERY_PARAM_NAME: &str = "code"; 94 | 95 | let Some(param) = get_query_param(url, STATE_QUERY_PARAM_NAME) else { 96 | anyhow::bail!("missing '{STATE_QUERY_PARAM_NAME}' query parameter"); 97 | }; 98 | 99 | let state = CsrfToken::new(param); 100 | 101 | let Some(param) = get_query_param(url, CODE_QUERY_PARAM_NAME) else { 102 | anyhow::bail!("missing '{CODE_QUERY_PARAM_NAME}' query parameter"); 103 | }; 104 | 105 | let code = AuthorizationCode::new(param); 106 | 107 | Ok((code, state)) 108 | } 109 | 110 | async fn oauth_http_client(req: oauth2::HttpRequest) -> Result { 111 | send::<_, http::Response>>(req).await 112 | } 113 | -------------------------------------------------------------------------------- /github-oauth/src/api/login.rs: -------------------------------------------------------------------------------- 1 | use spin_sdk::http::{Headers, OutgoingResponse, ResponseOutparam}; 2 | 3 | /// `login` returns the login page. 4 | pub async fn login(output: ResponseOutparam) { 5 | const LOGIN_HTML: &[u8] = include_bytes!("../../login.html"); // TODO: this shouldn't be included statically. 6 | 7 | let headers = Headers::new(); 8 | 9 | if let Err(err) = headers.set(&"content-type".to_string(), &[b"text/html".to_vec()]) { 10 | eprintln!("error setting content-type header: {err}"); 11 | return; 12 | } 13 | 14 | let response = OutgoingResponse::new(headers); 15 | response.set_status_code(200).unwrap(); 16 | 17 | if let Err(error) = output.set_with_body(response, LOGIN_HTML.to_vec()).await { 18 | eprintln!("error send login page: {error}"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /github-oauth/src/lib.rs: -------------------------------------------------------------------------------- 1 | use spin_sdk::http::{Headers, IncomingRequest, OutgoingResponse, ResponseOutparam}; 2 | use spin_sdk::http_component; 3 | use url::Url; 4 | 5 | mod api; 6 | 7 | // TODO: Allow configurable redirect URL 8 | #[http_component] 9 | async fn middleware(request: IncomingRequest, output: ResponseOutparam) { 10 | let url = match get_url(&request) { 11 | Ok(url) => url, 12 | Err(e) => { 13 | eprintln!("error parsing URL: {e}"); 14 | let response = OutgoingResponse::new(Headers::new()); 15 | response.set_status_code(500).unwrap(); 16 | output.set(response); 17 | return; 18 | } 19 | }; 20 | 21 | match url.path() { 22 | "/login/authorize" => api::authorize(output).await, 23 | "/login/callback" => api::callback(url, output).await, 24 | "/login" => api::login(output).await, 25 | _ => api::authenticate(request, output).await, 26 | } 27 | } 28 | 29 | fn get_url(request: &IncomingRequest) -> anyhow::Result { 30 | let authority = request 31 | .authority() 32 | .ok_or(anyhow::anyhow!("missing host header"))?; 33 | 34 | let path = request.path_with_query().unwrap_or_default(); 35 | let full = format!("http://{}{}", authority, path); 36 | Ok(Url::parse(&full)?) 37 | } 38 | 39 | wit_bindgen::generate!({ 40 | runtime_path: "::spin_sdk::wit_bindgen::rt", 41 | world: "wasi-http-import", 42 | path: "wits", 43 | with: { 44 | "wasi:http/types@0.2.0": spin_sdk::wit::wasi::http::types, 45 | "wasi:io/error@0.2.0": spin_executor::bindings::wasi::io::error, 46 | "wasi:io/streams@0.2.0": spin_executor::bindings::wasi::io::streams, 47 | "wasi:io/poll@0.2.0": spin_executor::bindings::wasi::io::poll, 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /github-oauth/wits/deps/cli/main.wit: -------------------------------------------------------------------------------- 1 | package wasi:cli@0.2.0; 2 | 3 | interface stdout { 4 | use wasi:io/streams@0.2.0.{output-stream}; 5 | 6 | get-stdout: func() -> output-stream; 7 | } 8 | 9 | interface stderr { 10 | use wasi:io/streams@0.2.0.{output-stream}; 11 | 12 | get-stderr: func() -> output-stream; 13 | } 14 | 15 | interface stdin { 16 | use wasi:io/streams@0.2.0.{input-stream}; 17 | 18 | get-stdin: func() -> input-stream; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /github-oauth/wits/deps/clocks/main.wit: -------------------------------------------------------------------------------- 1 | package wasi:clocks@0.2.0; 2 | 3 | interface monotonic-clock { 4 | use wasi:io/poll@0.2.0.{pollable}; 5 | 6 | type instant = u64; 7 | 8 | type duration = u64; 9 | 10 | now: func() -> instant; 11 | 12 | resolution: func() -> duration; 13 | 14 | subscribe-instant: func(when: instant) -> pollable; 15 | 16 | subscribe-duration: func(when: duration) -> pollable; 17 | } 18 | 19 | interface wall-clock { 20 | record datetime { 21 | seconds: u64, 22 | nanoseconds: u32, 23 | } 24 | 25 | now: func() -> datetime; 26 | 27 | resolution: func() -> datetime; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /github-oauth/wits/deps/http/main.wit: -------------------------------------------------------------------------------- 1 | package wasi:http@0.2.0; 2 | 3 | /// This interface defines all of the types and methods for implementing 4 | /// HTTP Requests and Responses, both incoming and outgoing, as well as 5 | /// their headers, trailers, and bodies. 6 | interface types { 7 | use wasi:clocks/monotonic-clock@0.2.0.{duration}; 8 | use wasi:io/streams@0.2.0.{input-stream, output-stream}; 9 | use wasi:io/error@0.2.0.{error as io-error}; 10 | use wasi:io/poll@0.2.0.{pollable}; 11 | 12 | /// This type corresponds to HTTP standard Methods. 13 | variant method { 14 | get, 15 | head, 16 | post, 17 | put, 18 | delete, 19 | connect, 20 | options, 21 | trace, 22 | patch, 23 | other(string), 24 | } 25 | 26 | /// This type corresponds to HTTP standard Related Schemes. 27 | variant scheme { 28 | HTTP, 29 | HTTPS, 30 | other(string), 31 | } 32 | 33 | /// Defines the case payload type for `DNS-error` above: 34 | record DNS-error-payload { 35 | rcode: option, 36 | info-code: option, 37 | } 38 | 39 | /// Defines the case payload type for `TLS-alert-received` above: 40 | record TLS-alert-received-payload { 41 | alert-id: option, 42 | alert-message: option, 43 | } 44 | 45 | /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: 46 | record field-size-payload { 47 | field-name: option, 48 | field-size: option, 49 | } 50 | 51 | /// These cases are inspired by the IANA HTTP Proxy Error Types: 52 | /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types 53 | variant error-code { 54 | DNS-timeout, 55 | DNS-error(DNS-error-payload), 56 | destination-not-found, 57 | destination-unavailable, 58 | destination-IP-prohibited, 59 | destination-IP-unroutable, 60 | connection-refused, 61 | connection-terminated, 62 | connection-timeout, 63 | connection-read-timeout, 64 | connection-write-timeout, 65 | connection-limit-reached, 66 | TLS-protocol-error, 67 | TLS-certificate-error, 68 | TLS-alert-received(TLS-alert-received-payload), 69 | HTTP-request-denied, 70 | HTTP-request-length-required, 71 | HTTP-request-body-size(option), 72 | HTTP-request-method-invalid, 73 | HTTP-request-URI-invalid, 74 | HTTP-request-URI-too-long, 75 | HTTP-request-header-section-size(option), 76 | HTTP-request-header-size(option), 77 | HTTP-request-trailer-section-size(option), 78 | HTTP-request-trailer-size(field-size-payload), 79 | HTTP-response-incomplete, 80 | HTTP-response-header-section-size(option), 81 | HTTP-response-header-size(field-size-payload), 82 | HTTP-response-body-size(option), 83 | HTTP-response-trailer-section-size(option), 84 | HTTP-response-trailer-size(field-size-payload), 85 | HTTP-response-transfer-coding(option), 86 | HTTP-response-content-coding(option), 87 | HTTP-response-timeout, 88 | HTTP-upgrade-failed, 89 | HTTP-protocol-error, 90 | loop-detected, 91 | configuration-error, 92 | /// This is a catch-all error for anything that doesn't fit cleanly into a 93 | /// more specific case. It also includes an optional string for an 94 | /// unstructured description of the error. Users should not depend on the 95 | /// string for diagnosing errors, as it's not required to be consistent 96 | /// between implementations. 97 | internal-error(option), 98 | } 99 | 100 | /// This type enumerates the different kinds of errors that may occur when 101 | /// setting or appending to a `fields` resource. 102 | variant header-error { 103 | /// This error indicates that a `field-key` or `field-value` was 104 | /// syntactically invalid when used with an operation that sets headers in a 105 | /// `fields`. 106 | invalid-syntax, 107 | /// This error indicates that a forbidden `field-key` was used when trying 108 | /// to set a header in a `fields`. 109 | forbidden, 110 | /// This error indicates that the operation on the `fields` was not 111 | /// permitted because the fields are immutable. 112 | immutable, 113 | } 114 | 115 | /// Field keys are always strings. 116 | type field-key = string; 117 | 118 | /// Field values should always be ASCII strings. However, in 119 | /// reality, HTTP implementations often have to interpret malformed values, 120 | /// so they are provided as a list of bytes. 121 | type field-value = list; 122 | 123 | /// This following block defines the `fields` resource which corresponds to 124 | /// HTTP standard Fields. Fields are a common representation used for both 125 | /// Headers and Trailers. 126 | /// 127 | /// A `fields` may be mutable or immutable. A `fields` created using the 128 | /// constructor, `from-list`, or `clone` will be mutable, but a `fields` 129 | /// resource given by other means (including, but not limited to, 130 | /// `incoming-request.headers`, `outgoing-request.headers`) might be be 131 | /// immutable. In an immutable fields, the `set`, `append`, and `delete` 132 | /// operations will fail with `header-error.immutable`. 133 | resource fields { 134 | /// Construct an empty HTTP Fields. 135 | /// 136 | /// The resulting `fields` is mutable. 137 | constructor(); 138 | /// Construct an HTTP Fields. 139 | /// 140 | /// The resulting `fields` is mutable. 141 | /// 142 | /// The list represents each key-value pair in the Fields. Keys 143 | /// which have multiple values are represented by multiple entries in this 144 | /// list with the same key. 145 | /// 146 | /// The tuple is a pair of the field key, represented as a string, and 147 | /// Value, represented as a list of bytes. In a valid Fields, all keys 148 | /// and values are valid UTF-8 strings. However, values are not always 149 | /// well-formed, so they are represented as a raw list of bytes. 150 | /// 151 | /// An error result will be returned if any header or value was 152 | /// syntactically invalid, or if a header was forbidden. 153 | from-list: static func(entries: list>) -> result; 154 | /// Get all of the values corresponding to a key. If the key is not present 155 | /// in this `fields`, an empty list is returned. However, if the key is 156 | /// present but empty, this is represented by a list with one or more 157 | /// empty field-values present. 158 | get: func(name: field-key) -> list; 159 | /// Returns `true` when the key is present in this `fields`. If the key is 160 | /// syntactically invalid, `false` is returned. 161 | has: func(name: field-key) -> bool; 162 | /// Set all of the values for a key. Clears any existing values for that 163 | /// key, if they have been set. 164 | /// 165 | /// Fails with `header-error.immutable` if the `fields` are immutable. 166 | set: func(name: field-key, value: list) -> result<_, header-error>; 167 | /// Delete all values for a key. Does nothing if no values for the key 168 | /// exist. 169 | /// 170 | /// Fails with `header-error.immutable` if the `fields` are immutable. 171 | delete: func(name: field-key) -> result<_, header-error>; 172 | /// Append a value for a key. Does not change or delete any existing 173 | /// values for that key. 174 | /// 175 | /// Fails with `header-error.immutable` if the `fields` are immutable. 176 | append: func(name: field-key, value: field-value) -> result<_, header-error>; 177 | /// Retrieve the full set of keys and values in the Fields. Like the 178 | /// constructor, the list represents each key-value pair. 179 | /// 180 | /// The outer list represents each key-value pair in the Fields. Keys 181 | /// which have multiple values are represented by multiple entries in this 182 | /// list with the same key. 183 | entries: func() -> list>; 184 | /// Make a deep copy of the Fields. Equivelant in behavior to calling the 185 | /// `fields` constructor on the return value of `entries`. The resulting 186 | /// `fields` is mutable. 187 | clone: func() -> fields; 188 | } 189 | 190 | /// Headers is an alias for Fields. 191 | type headers = fields; 192 | 193 | /// Trailers is an alias for Fields. 194 | type trailers = fields; 195 | 196 | /// Represents an incoming HTTP Request. 197 | resource incoming-request { 198 | /// Returns the method of the incoming request. 199 | method: func() -> method; 200 | /// Returns the path with query parameters from the request, as a string. 201 | path-with-query: func() -> option; 202 | /// Returns the protocol scheme from the request. 203 | scheme: func() -> option; 204 | /// Returns the authority from the request, if it was present. 205 | authority: func() -> option; 206 | /// Get the `headers` associated with the request. 207 | /// 208 | /// The returned `headers` resource is immutable: `set`, `append`, and 209 | /// `delete` operations will fail with `header-error.immutable`. 210 | /// 211 | /// The `headers` returned are a child resource: it must be dropped before 212 | /// the parent `incoming-request` is dropped. Dropping this 213 | /// `incoming-request` before all children are dropped will trap. 214 | headers: func() -> headers; 215 | /// Gives the `incoming-body` associated with this request. Will only 216 | /// return success at most once, and subsequent calls will return error. 217 | consume: func() -> result; 218 | } 219 | 220 | /// Represents an outgoing HTTP Request. 221 | resource outgoing-request { 222 | /// Construct a new `outgoing-request` with a default `method` of `GET`, and 223 | /// `none` values for `path-with-query`, `scheme`, and `authority`. 224 | /// 225 | /// * `headers` is the HTTP Headers for the Request. 226 | /// 227 | /// It is possible to construct, or manipulate with the accessor functions 228 | /// below, an `outgoing-request` with an invalid combination of `scheme` 229 | /// and `authority`, or `headers` which are not permitted to be sent. 230 | /// It is the obligation of the `outgoing-handler.handle` implementation 231 | /// to reject invalid constructions of `outgoing-request`. 232 | constructor(headers: headers); 233 | /// Returns the resource corresponding to the outgoing Body for this 234 | /// Request. 235 | /// 236 | /// Returns success on the first call: the `outgoing-body` resource for 237 | /// this `outgoing-request` can be retrieved at most once. Subsequent 238 | /// calls will return error. 239 | body: func() -> result; 240 | /// Get the Method for the Request. 241 | method: func() -> method; 242 | /// Set the Method for the Request. Fails if the string present in a 243 | /// `method.other` argument is not a syntactically valid method. 244 | set-method: func(method: method) -> result; 245 | /// Get the combination of the HTTP Path and Query for the Request. 246 | /// When `none`, this represents an empty Path and empty Query. 247 | path-with-query: func() -> option; 248 | /// Set the combination of the HTTP Path and Query for the Request. 249 | /// When `none`, this represents an empty Path and empty Query. Fails is the 250 | /// string given is not a syntactically valid path and query uri component. 251 | set-path-with-query: func(path-with-query: option) -> result; 252 | /// Get the HTTP Related Scheme for the Request. When `none`, the 253 | /// implementation may choose an appropriate default scheme. 254 | scheme: func() -> option; 255 | /// Set the HTTP Related Scheme for the Request. When `none`, the 256 | /// implementation may choose an appropriate default scheme. Fails if the 257 | /// string given is not a syntactically valid uri scheme. 258 | set-scheme: func(scheme: option) -> result; 259 | /// Get the HTTP Authority for the Request. A value of `none` may be used 260 | /// with Related Schemes which do not require an Authority. The HTTP and 261 | /// HTTPS schemes always require an authority. 262 | authority: func() -> option; 263 | /// Set the HTTP Authority for the Request. A value of `none` may be used 264 | /// with Related Schemes which do not require an Authority. The HTTP and 265 | /// HTTPS schemes always require an authority. Fails if the string given is 266 | /// not a syntactically valid uri authority. 267 | set-authority: func(authority: option) -> result; 268 | /// Get the headers associated with the Request. 269 | /// 270 | /// The returned `headers` resource is immutable: `set`, `append`, and 271 | /// `delete` operations will fail with `header-error.immutable`. 272 | /// 273 | /// This headers resource is a child: it must be dropped before the parent 274 | /// `outgoing-request` is dropped, or its ownership is transfered to 275 | /// another component by e.g. `outgoing-handler.handle`. 276 | headers: func() -> headers; 277 | } 278 | 279 | /// Parameters for making an HTTP Request. Each of these parameters is 280 | /// currently an optional timeout applicable to the transport layer of the 281 | /// HTTP protocol. 282 | /// 283 | /// These timeouts are separate from any the user may use to bound a 284 | /// blocking call to `wasi:io/poll.poll`. 285 | resource request-options { 286 | /// Construct a default `request-options` value. 287 | constructor(); 288 | /// The timeout for the initial connect to the HTTP Server. 289 | connect-timeout: func() -> option; 290 | /// Set the timeout for the initial connect to the HTTP Server. An error 291 | /// return value indicates that this timeout is not supported. 292 | set-connect-timeout: func(duration: option) -> result; 293 | /// The timeout for receiving the first byte of the Response body. 294 | first-byte-timeout: func() -> option; 295 | /// Set the timeout for receiving the first byte of the Response body. An 296 | /// error return value indicates that this timeout is not supported. 297 | set-first-byte-timeout: func(duration: option) -> result; 298 | /// The timeout for receiving subsequent chunks of bytes in the Response 299 | /// body stream. 300 | between-bytes-timeout: func() -> option; 301 | /// Set the timeout for receiving subsequent chunks of bytes in the Response 302 | /// body stream. An error return value indicates that this timeout is not 303 | /// supported. 304 | set-between-bytes-timeout: func(duration: option) -> result; 305 | } 306 | 307 | /// Represents the ability to send an HTTP Response. 308 | /// 309 | /// This resource is used by the `wasi:http/incoming-handler` interface to 310 | /// allow a Response to be sent corresponding to the Request provided as the 311 | /// other argument to `incoming-handler.handle`. 312 | resource response-outparam { 313 | /// Set the value of the `response-outparam` to either send a response, 314 | /// or indicate an error. 315 | /// 316 | /// This method consumes the `response-outparam` to ensure that it is 317 | /// called at most once. If it is never called, the implementation 318 | /// will respond with an error. 319 | /// 320 | /// The user may provide an `error` to `response` to allow the 321 | /// implementation determine how to respond with an HTTP error response. 322 | set: static func(param: response-outparam, response: result); 323 | } 324 | 325 | /// This type corresponds to the HTTP standard Status Code. 326 | type status-code = u16; 327 | 328 | /// Represents an incoming HTTP Response. 329 | resource incoming-response { 330 | /// Returns the status code from the incoming response. 331 | status: func() -> status-code; 332 | /// Returns the headers from the incoming response. 333 | /// 334 | /// The returned `headers` resource is immutable: `set`, `append`, and 335 | /// `delete` operations will fail with `header-error.immutable`. 336 | /// 337 | /// This headers resource is a child: it must be dropped before the parent 338 | /// `incoming-response` is dropped. 339 | headers: func() -> headers; 340 | /// Returns the incoming body. May be called at most once. Returns error 341 | /// if called additional times. 342 | consume: func() -> result; 343 | } 344 | 345 | /// Represents an incoming HTTP Request or Response's Body. 346 | /// 347 | /// A body has both its contents - a stream of bytes - and a (possibly 348 | /// empty) set of trailers, indicating that the full contents of the 349 | /// body have been received. This resource represents the contents as 350 | /// an `input-stream` and the delivery of trailers as a `future-trailers`, 351 | /// and ensures that the user of this interface may only be consuming either 352 | /// the body contents or waiting on trailers at any given time. 353 | resource incoming-body { 354 | /// Returns the contents of the body, as a stream of bytes. 355 | /// 356 | /// Returns success on first call: the stream representing the contents 357 | /// can be retrieved at most once. Subsequent calls will return error. 358 | /// 359 | /// The returned `input-stream` resource is a child: it must be dropped 360 | /// before the parent `incoming-body` is dropped, or consumed by 361 | /// `incoming-body.finish`. 362 | /// 363 | /// This invariant ensures that the implementation can determine whether 364 | /// the user is consuming the contents of the body, waiting on the 365 | /// `future-trailers` to be ready, or neither. This allows for network 366 | /// backpressure is to be applied when the user is consuming the body, 367 | /// and for that backpressure to not inhibit delivery of the trailers if 368 | /// the user does not read the entire body. 369 | %stream: func() -> result; 370 | /// Takes ownership of `incoming-body`, and returns a `future-trailers`. 371 | /// This function will trap if the `input-stream` child is still alive. 372 | finish: static func(this: incoming-body) -> future-trailers; 373 | } 374 | 375 | /// Represents a future which may eventaully return trailers, or an error. 376 | /// 377 | /// In the case that the incoming HTTP Request or Response did not have any 378 | /// trailers, this future will resolve to the empty set of trailers once the 379 | /// complete Request or Response body has been received. 380 | resource future-trailers { 381 | /// Returns a pollable which becomes ready when either the trailers have 382 | /// been received, or an error has occured. When this pollable is ready, 383 | /// the `get` method will return `some`. 384 | subscribe: func() -> pollable; 385 | /// Returns the contents of the trailers, or an error which occured, 386 | /// once the future is ready. 387 | /// 388 | /// The outer `option` represents future readiness. Users can wait on this 389 | /// `option` to become `some` using the `subscribe` method. 390 | /// 391 | /// The outer `result` is used to retrieve the trailers or error at most 392 | /// once. It will be success on the first call in which the outer option 393 | /// is `some`, and error on subsequent calls. 394 | /// 395 | /// The inner `result` represents that either the HTTP Request or Response 396 | /// body, as well as any trailers, were received successfully, or that an 397 | /// error occured receiving them. The optional `trailers` indicates whether 398 | /// or not trailers were present in the body. 399 | /// 400 | /// When some `trailers` are returned by this method, the `trailers` 401 | /// resource is immutable, and a child. Use of the `set`, `append`, or 402 | /// `delete` methods will return an error, and the resource must be 403 | /// dropped before the parent `future-trailers` is dropped. 404 | get: func() -> option, error-code>>>; 405 | } 406 | 407 | /// Represents an outgoing HTTP Response. 408 | resource outgoing-response { 409 | /// Construct an `outgoing-response`, with a default `status-code` of `200`. 410 | /// If a different `status-code` is needed, it must be set via the 411 | /// `set-status-code` method. 412 | /// 413 | /// * `headers` is the HTTP Headers for the Response. 414 | constructor(headers: headers); 415 | /// Get the HTTP Status Code for the Response. 416 | status-code: func() -> status-code; 417 | /// Set the HTTP Status Code for the Response. Fails if the status-code 418 | /// given is not a valid http status code. 419 | set-status-code: func(status-code: status-code) -> result; 420 | /// Get the headers associated with the Request. 421 | /// 422 | /// The returned `headers` resource is immutable: `set`, `append`, and 423 | /// `delete` operations will fail with `header-error.immutable`. 424 | /// 425 | /// This headers resource is a child: it must be dropped before the parent 426 | /// `outgoing-request` is dropped, or its ownership is transfered to 427 | /// another component by e.g. `outgoing-handler.handle`. 428 | headers: func() -> headers; 429 | /// Returns the resource corresponding to the outgoing Body for this Response. 430 | /// 431 | /// Returns success on the first call: the `outgoing-body` resource for 432 | /// this `outgoing-response` can be retrieved at most once. Subsequent 433 | /// calls will return error. 434 | body: func() -> result; 435 | } 436 | 437 | /// Represents an outgoing HTTP Request or Response's Body. 438 | /// 439 | /// A body has both its contents - a stream of bytes - and a (possibly 440 | /// empty) set of trailers, inducating the full contents of the body 441 | /// have been sent. This resource represents the contents as an 442 | /// `output-stream` child resource, and the completion of the body (with 443 | /// optional trailers) with a static function that consumes the 444 | /// `outgoing-body` resource, and ensures that the user of this interface 445 | /// may not write to the body contents after the body has been finished. 446 | /// 447 | /// If the user code drops this resource, as opposed to calling the static 448 | /// method `finish`, the implementation should treat the body as incomplete, 449 | /// and that an error has occured. The implementation should propogate this 450 | /// error to the HTTP protocol by whatever means it has available, 451 | /// including: corrupting the body on the wire, aborting the associated 452 | /// Request, or sending a late status code for the Response. 453 | resource outgoing-body { 454 | /// Returns a stream for writing the body contents. 455 | /// 456 | /// The returned `output-stream` is a child resource: it must be dropped 457 | /// before the parent `outgoing-body` resource is dropped (or finished), 458 | /// otherwise the `outgoing-body` drop or `finish` will trap. 459 | /// 460 | /// Returns success on the first call: the `output-stream` resource for 461 | /// this `outgoing-body` may be retrieved at most once. Subsequent calls 462 | /// will return error. 463 | write: func() -> result; 464 | /// Finalize an outgoing body, optionally providing trailers. This must be 465 | /// called to signal that the response is complete. If the `outgoing-body` 466 | /// is dropped without calling `outgoing-body.finalize`, the implementation 467 | /// should treat the body as corrupted. 468 | /// 469 | /// Fails if the body's `outgoing-request` or `outgoing-response` was 470 | /// constructed with a Content-Length header, and the contents written 471 | /// to the body (via `write`) does not match the value given in the 472 | /// Content-Length. 473 | finish: static func(this: outgoing-body, trailers: option) -> result<_, error-code>; 474 | } 475 | 476 | /// Represents a future which may eventaully return an incoming HTTP 477 | /// Response, or an error. 478 | /// 479 | /// This resource is returned by the `wasi:http/outgoing-handler` interface to 480 | /// provide the HTTP Response corresponding to the sent Request. 481 | resource future-incoming-response { 482 | /// Returns a pollable which becomes ready when either the Response has 483 | /// been received, or an error has occured. When this pollable is ready, 484 | /// the `get` method will return `some`. 485 | subscribe: func() -> pollable; 486 | /// Returns the incoming HTTP Response, or an error, once one is ready. 487 | /// 488 | /// The outer `option` represents future readiness. Users can wait on this 489 | /// `option` to become `some` using the `subscribe` method. 490 | /// 491 | /// The outer `result` is used to retrieve the response or error at most 492 | /// once. It will be success on the first call in which the outer option 493 | /// is `some`, and error on subsequent calls. 494 | /// 495 | /// The inner `result` represents that either the incoming HTTP Response 496 | /// status and headers have recieved successfully, or that an error 497 | /// occured. Errors may also occur while consuming the response body, 498 | /// but those will be reported by the `incoming-body` and its 499 | /// `output-stream` child. 500 | get: func() -> option>>; 501 | } 502 | 503 | /// Attempts to extract a http-related `error` from the wasi:io `error` 504 | /// provided. 505 | /// 506 | /// Stream operations which return 507 | /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of 508 | /// type `wasi:io/error/error` with more information about the operation 509 | /// that failed. This payload can be passed through to this function to see 510 | /// if there's http-related information about the error to return. 511 | /// 512 | /// Note that this function is fallible because not all io-errors are 513 | /// http-related errors. 514 | http-error-code: func(err: borrow) -> option; 515 | } 516 | 517 | /// This interface defines a handler of incoming HTTP Requests. It should 518 | /// be exported by components which can respond to HTTP Requests. 519 | interface incoming-handler { 520 | use types.{incoming-request, response-outparam}; 521 | 522 | /// This function is invoked with an incoming HTTP Request, and a resource 523 | /// `response-outparam` which provides the capability to reply with an HTTP 524 | /// Response. The response is sent by calling the `response-outparam.set` 525 | /// method, which allows execution to continue after the response has been 526 | /// sent. This enables both streaming to the response body, and performing other 527 | /// work. 528 | /// 529 | /// The implementor of this function must write a response to the 530 | /// `response-outparam` before returning, or else the caller will respond 531 | /// with an error on its behalf. 532 | handle: func(request: incoming-request, response-out: response-outparam); 533 | } 534 | 535 | /// This interface defines a handler of outgoing HTTP Requests. It should be 536 | /// imported by components which wish to make HTTP Requests. 537 | interface outgoing-handler { 538 | use types.{outgoing-request, request-options, future-incoming-response, error-code}; 539 | 540 | /// This function is invoked with an outgoing HTTP Request, and it returns 541 | /// a resource `future-incoming-response` which represents an HTTP Response 542 | /// which may arrive in the future. 543 | /// 544 | /// The `options` argument accepts optional parameters for the HTTP 545 | /// protocol's transport layer. 546 | /// 547 | /// This function may return an error if the `outgoing-request` is invalid 548 | /// or not allowed to be made. Otherwise, protocol errors are reported 549 | /// through the `future-incoming-response`. 550 | handle: func(request: outgoing-request, options: option) -> result; 551 | } 552 | 553 | /// The `wasi:http/proxy` world captures a widely-implementable intersection of 554 | /// hosts that includes HTTP forward and reverse proxies. Components targeting 555 | /// this world may concurrently stream in and out any number of incoming and 556 | /// outgoing HTTP requests. 557 | world proxy { 558 | import wasi:random/random@0.2.0; 559 | import wasi:io/error@0.2.0; 560 | import wasi:io/poll@0.2.0; 561 | import wasi:io/streams@0.2.0; 562 | import wasi:cli/stdout@0.2.0; 563 | import wasi:cli/stderr@0.2.0; 564 | import wasi:cli/stdin@0.2.0; 565 | import wasi:clocks/monotonic-clock@0.2.0; 566 | import types; 567 | import outgoing-handler; 568 | import wasi:clocks/wall-clock@0.2.0; 569 | 570 | export incoming-handler; 571 | } 572 | -------------------------------------------------------------------------------- /github-oauth/wits/deps/io/main.wit: -------------------------------------------------------------------------------- 1 | package wasi:io@0.2.0; 2 | 3 | interface poll { 4 | resource pollable { 5 | ready: func() -> bool; 6 | block: func(); 7 | } 8 | 9 | poll: func(in: list>) -> list; 10 | } 11 | 12 | interface error { 13 | resource error { 14 | to-debug-string: func() -> string; 15 | } 16 | } 17 | 18 | interface streams { 19 | use error.{error}; 20 | use poll.{pollable}; 21 | 22 | variant stream-error { 23 | last-operation-failed(error), 24 | closed, 25 | } 26 | 27 | resource input-stream { 28 | read: func(len: u64) -> result, stream-error>; 29 | blocking-read: func(len: u64) -> result, stream-error>; 30 | skip: func(len: u64) -> result; 31 | blocking-skip: func(len: u64) -> result; 32 | subscribe: func() -> pollable; 33 | } 34 | 35 | resource output-stream { 36 | check-write: func() -> result; 37 | write: func(contents: list) -> result<_, stream-error>; 38 | blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; 39 | flush: func() -> result<_, stream-error>; 40 | blocking-flush: func() -> result<_, stream-error>; 41 | subscribe: func() -> pollable; 42 | write-zeroes: func(len: u64) -> result<_, stream-error>; 43 | blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; 44 | splice: func(src: borrow, len: u64) -> result; 45 | blocking-splice: func(src: borrow, len: u64) -> result; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /github-oauth/wits/deps/random/main.wit: -------------------------------------------------------------------------------- 1 | package wasi:random@0.2.0; 2 | 3 | interface random { 4 | get-random-bytes: func(len: u64) -> list; 5 | 6 | get-random-u64: func() -> u64; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /github-oauth/wits/main.wit: -------------------------------------------------------------------------------- 1 | package middleware:http-auth; 2 | 3 | world wasi-http-import { 4 | import wasi:http/incoming-handler@0.2.0; 5 | } -------------------------------------------------------------------------------- /spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "github-oauth2-example" 5 | version = "0.1.0" 6 | description = "A simple HTTP handler" 7 | 8 | [[trigger.http]] 9 | route = "/..." 10 | component = "frontend" 11 | 12 | [component.frontend] 13 | source = "target/wasm32-wasi/release/github_oauth.wasm" 14 | allowed_outbound_hosts = ["https://github.com", "https://api.github.com"] 15 | [component.frontend.build] 16 | command = "./build.sh" 17 | [component.frontend.dependencies] 18 | "wasi:http/incoming-handler@0.2.0" = { path = "target/wasm32-wasi/release/example.wasm" } 19 | --------------------------------------------------------------------------------