all repos

onasty @ 327757c9371a333d3436f0a204677742a04236cf

a one-time notes service
29 files changed, 1846 insertions(+), 0 deletions(-)
scaffold frontend app (#134)

* chore: boot strap elm

* bootstrap for /sign-in

* web: add sendApiRequest effect

* implement /sign-in

* add logout button

* web: implement token refreshing

* web: update elm-land's Auth

* test: the credentials decoder

* chore: work on linters errors

* add protected page

* chore(ci): add elm

* fixup! add protected page

* refactor: update expiration threshold for tokens

* chore(ci): add cache for elm tools

* chore: delete all classes since i dont have any css yet

* web: don't allow authorized user to access sign in page
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-18 17:25:55 +0300
Parent: f4de4b3
A .github/workflows/elm.yml
···
        
        1
        +name: elm

      
        
        2
        +

      
        
        3
        +on:

      
        
        4
        +  workflow_dispatch:

      
        
        5
        +  push:

      
        
        6
        +    branches: [main]

      
        
        7
        +    paths: ["web/**"]

      
        
        8
        +  pull_request:

      
        
        9
        +    paths: ["web/**"]

      
        
        10
        +

      
        
        11
        +

      
        
        12
        +jobs:

      
        
        13
        +  release:

      
        
        14
        +    runs-on: ubuntu-latest

      
        
        15
        +    defaults:

      
        
        16
        +      run:

      
        
        17
        +        working-directory: web

      
        
        18
        +    steps:

      
        
        19
        +      - uses: actions/checkout@v4

      
        
        20
        +      - uses: oven-sh/setup-bun@v2

      
        
        21
        +

      
        
        22
        +      - name: Install deps

      
        
        23
        +        run: bun install --frozen-lockfile

      
        
        24
        +

      
        
        25
        +      - name: Elm cache

      
        
        26
        +        uses: actions/cache@v4

      
        
        27
        +        with:

      
        
        28
        +          path: ~/.elm

      
        
        29
        +          key: elm-${{ runner.os }}-${{ hashFiles('web/elm.json') }}

      
        
        30
        +          restore-keys: |

      
        
        31
        +            elm-${{ runner.os }}-

      
        
        32
        +

      
        
        33
        +      - name: Build

      
        
        34
        +        run: bunx elm-land build

      
        
        35
        +

      
        
        36
        +      - name: elm-review

      
        
        37
        +        run: bunx elm-review --ignore-dirs .elm-land

      
        
        38
        +

      
        
        39
        +      - name: Tests

      
        
        40
        +        run: bunx elm-test-rs

      
        
        41
        +

      
M Taskfile.yml
···
        5
        5
         

      
        6
        6
         includes:

      
        7
        7
           migrate: ./migrations/Taskfile.yml

      
        
        8
        +  frontend:

      
        
        9
        +    taskfile: ./web/Taskfile.yml

      
        
        10
        +    dir: ./web/

      
        8
        11
         

      
        9
        12
         env:

      
        10
        13
           COMPOSE_BAKE: 1

      
A web/.gitignore
···
        
        1
        +elm-stuff/

      
        
        2
        +.elm-land/

      
        
        3
        +/dist/

      
        
        4
        +/node_modules/

      
        
        5
        +*.pem

      
        
        6
        +/.env

      
A web/Taskfile.yml
···
        
        1
        +version: "3"

      
        
        2
        +

      
        
        3
        +tasks:

      
        
        4
        +  install:

      
        
        5
        +    desc: install dependencies

      
        
        6
        +    cmd: bun install

      
        
        7
        +

      
        
        8
        +  lint:

      
        
        9
        +    desc: runs elm-review

      
        
        10
        +    cmd: bunx elm-review --ignore-dirs .elm-land

      
        
        11
        +

      
        
        12
        +  lint:fix:

      
        
        13
        +    desc: runs elm-review fix

      
        
        14
        +    cmd: bunx elm-review --ignore-dirs .elm-land --fix

      
        
        15
        +

      
        
        16
        +  test:

      
        
        17
        +    desc: run tests

      
        
        18
        +    cmd: bunx elm-test-rs

      
        
        19
        +

      
        
        20
        +  dev:

      
        
        21
        +    desc: runs elm-land dev server

      
        
        22
        +    cmd: bunx elm-land server

      
A web/bun.lock
···
        
        1
        +{

      
        
        2
        +  "lockfileVersion": 1,

      
        
        3
        +  "workspaces": {

      
        
        4
        +    "": {

      
        
        5
        +      "name": "web",

      
        
        6
        +      "devDependencies": {

      
        
        7
        +        "elm-land": "^0.20.1",

      
        
        8
        +        "elm-review": "^2.13.2",

      
        
        9
        +        "elm-tooling": "^1.15.1",

      
        
        10
        +      },

      
        
        11
        +    },

      
        
        12
        +  },

      
        
        13
        +  "packages": {

      
        
        14
        +    "@elm_binaries/darwin_arm64": ["@elm_binaries/darwin_arm64@0.19.1-0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjbsH7BNHEAmoE2SCJFcfk5fIHwFIpxtSgnEAqMsVLpBUFoEtAeX+LQ+N0vSFJB3WAh73+QYx/xSluxxLcL6dA=="],

      
        
        15
        +

      
        
        16
        +    "@elm_binaries/darwin_x64": ["@elm_binaries/darwin_x64@0.19.1-0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGUtrZTPBzaxgi9al6nr+9313wrnUVHuijzUK39UsPS+pa+n6CmWyV/69sHZeX9qy6UfeugE0PzF3qcUiy2GDQ=="],

      
        
        17
        +

      
        
        18
        +    "@elm_binaries/linux_x64": ["@elm_binaries/linux_x64@0.19.1-0", "", { "os": "linux", "cpu": "x64" }, "sha512-T1ZrWVhg2kKAsi8caOd3vp/1A3e21VuCpSG63x8rDie50fHbCytTway9B8WHEdnBFv4mYWiA68dzGxYCiFmU2w=="],

      
        
        19
        +

      
        
        20
        +    "@elm_binaries/win32_x64": ["@elm_binaries/win32_x64@0.19.1-0", "", { "os": "win32", "cpu": "x64" }, "sha512-yDleiXqSE9EcqKtd9SkC/4RIW8I71YsXzMPL79ub2bBPHjWTcoyyeBbYjoOB9SxSlArJ74HaoBApzT6hY7Zobg=="],

      
        
        21
        +

      
        
        22
        +    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.20.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g=="],

      
        
        23
        +

      
        
        24
        +    "@esbuild/android-arm": ["@esbuild/android-arm@0.20.2", "", { "os": "android", "cpu": "arm" }, "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w=="],

      
        
        25
        +

      
        
        26
        +    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.20.2", "", { "os": "android", "cpu": "arm64" }, "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg=="],

      
        
        27
        +

      
        
        28
        +    "@esbuild/android-x64": ["@esbuild/android-x64@0.20.2", "", { "os": "android", "cpu": "x64" }, "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg=="],

      
        
        29
        +

      
        
        30
        +    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.20.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA=="],

      
        
        31
        +

      
        
        32
        +    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.20.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA=="],

      
        
        33
        +

      
        
        34
        +    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.20.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw=="],

      
        
        35
        +

      
        
        36
        +    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.20.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw=="],

      
        
        37
        +

      
        
        38
        +    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.20.2", "", { "os": "linux", "cpu": "arm" }, "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg=="],

      
        
        39
        +

      
        
        40
        +    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.20.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A=="],

      
        
        41
        +

      
        
        42
        +    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.20.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig=="],

      
        
        43
        +

      
        
        44
        +    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.20.2", "", { "os": "linux", "cpu": "none" }, "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ=="],

      
        
        45
        +

      
        
        46
        +    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.20.2", "", { "os": "linux", "cpu": "none" }, "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA=="],

      
        
        47
        +

      
        
        48
        +    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.20.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg=="],

      
        
        49
        +

      
        
        50
        +    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.20.2", "", { "os": "linux", "cpu": "none" }, "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg=="],

      
        
        51
        +

      
        
        52
        +    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.20.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ=="],

      
        
        53
        +

      
        
        54
        +    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.20.2", "", { "os": "linux", "cpu": "x64" }, "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw=="],

      
        
        55
        +

      
        
        56
        +    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.20.2", "", { "os": "none", "cpu": "x64" }, "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ=="],

      
        
        57
        +

      
        
        58
        +    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.20.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ=="],

      
        
        59
        +

      
        
        60
        +    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.20.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w=="],

      
        
        61
        +

      
        
        62
        +    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.20.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ=="],

      
        
        63
        +

      
        
        64
        +    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.20.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ=="],

      
        
        65
        +

      
        
        66
        +    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.20.2", "", { "os": "win32", "cpu": "x64" }, "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ=="],

      
        
        67
        +

      
        
        68
        +    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],

      
        
        69
        +

      
        
        70
        +    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],

      
        
        71
        +

      
        
        72
        +    "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],

      
        
        73
        +

      
        
        74
        +    "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="],

      
        
        75
        +

      
        
        76
        +    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],

      
        
        77
        +

      
        
        78
        +    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],

      
        
        79
        +

      
        
        80
        +    "@lydell/elm": ["@lydell/elm@0.19.1-14", "", { "optionalDependencies": { "@lydell/elm_darwin_arm64": "0.19.1-3", "@lydell/elm_darwin_x64": "0.19.1-2", "@lydell/elm_linux_arm": "0.19.1-0", "@lydell/elm_linux_arm64": "0.19.1-4", "@lydell/elm_linux_x64": "0.19.1-1", "@lydell/elm_win32_x64": "0.19.1-1" }, "bin": { "elm": "bin/elm" } }, "sha512-otpGlYiNRvL7F9k6MJOTcuyIgHr+XWy/1NtHpGUgQi8lHrnuyCjwKFPPiimKpr3bcZTwpD4nebHuYR0bmPIKuA=="],

      
        
        81
        +

      
        
        82
        +    "@lydell/elm_darwin_arm64": ["@lydell/elm_darwin_arm64@0.19.1-3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RuKTz5ck+RBx4urj1EL/r0xWZZqBMPEXzNBQTEBCAMWLSi4Ck3TVz5pkhBaK+cRZXI+cCgytm/1bIttbp2fFIg=="],

      
        
        83
        +

      
        
        84
        +    "@lydell/elm_darwin_x64": ["@lydell/elm_darwin_x64@0.19.1-2", "", { "os": "darwin", "cpu": "x64" }, "sha512-MXfQwxdQfmuQ22iDCFlcXu5YTA0w6/ASzbxmWc+8DkgUkHTynjViGBVkQljAbYe4ZWgrYGWinZQQyhVnp/5oZw=="],

      
        
        85
        +

      
        
        86
        +    "@lydell/elm_linux_arm": ["@lydell/elm_linux_arm@0.19.1-0", "", { "os": "linux", "cpu": "arm" }, "sha512-crKrLzuT6jn4OOS7PWKZGYFw6vHwPu3iNP7lg8rFkOog/HxlkRwX4S695aILBG8SGTLhEdfP9tg28SQ7vR4Lpg=="],

      
        
        87
        +

      
        
        88
        +    "@lydell/elm_linux_arm64": ["@lydell/elm_linux_arm64@0.19.1-4", "", { "os": "linux", "cpu": "arm64" }, "sha512-JuUkkVBtJjUajtTriQFFANHDmwA14NhqNqgIcq5LCJ6vUQv5/LVd6NUOkl/Rdq7Ju/VN/XwBD1/vm7MGIMOTqA=="],

      
        
        89
        +

      
        
        90
        +    "@lydell/elm_linux_x64": ["@lydell/elm_linux_x64@0.19.1-1", "", { "os": "linux", "cpu": "x64" }, "sha512-1Y8UAb+GfUqlSjUTX9CaaZhJqvhVcfNbYC0N9AEutlXf1CzFMvF4VsDeZdxzhNI4allPRWBD1IqtdlLhBTFacA=="],

      
        
        91
        +

      
        
        92
        +    "@lydell/elm_win32_x64": ["@lydell/elm_win32_x64@0.19.1-1", "", { "os": "win32", "cpu": "x64" }, "sha512-3LMiJ+uUxDFLNnCd6HBmvVWSjSWjs/Z9dMXZWCMOcw3vrW9iOkRrsNGNxohRXun2YRd8wXOX8/DwVn8i2SJ3KA=="],

      
        
        93
        +

      
        
        94
        +    "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.43.0", "", { "os": "android", "cpu": "arm" }, "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw=="],

      
        
        95
        +

      
        
        96
        +    "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.43.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA=="],

      
        
        97
        +

      
        
        98
        +    "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A=="],

      
        
        99
        +

      
        
        100
        +    "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg=="],

      
        
        101
        +

      
        
        102
        +    "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.43.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ=="],

      
        
        103
        +

      
        
        104
        +    "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.43.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg=="],

      
        
        105
        +

      
        
        106
        +    "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw=="],

      
        
        107
        +

      
        
        108
        +    "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw=="],

      
        
        109
        +

      
        
        110
        +    "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA=="],

      
        
        111
        +

      
        
        112
        +    "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA=="],

      
        
        113
        +

      
        
        114
        +    "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg=="],

      
        
        115
        +

      
        
        116
        +    "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.43.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw=="],

      
        
        117
        +

      
        
        118
        +    "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g=="],

      
        
        119
        +

      
        
        120
        +    "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q=="],

      
        
        121
        +

      
        
        122
        +    "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.43.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw=="],

      
        
        123
        +

      
        
        124
        +    "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ=="],

      
        
        125
        +

      
        
        126
        +    "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ=="],

      
        
        127
        +

      
        
        128
        +    "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw=="],

      
        
        129
        +

      
        
        130
        +    "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.43.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw=="],

      
        
        131
        +

      
        
        132
        +    "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw=="],

      
        
        133
        +

      
        
        134
        +    "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],

      
        
        135
        +

      
        
        136
        +    "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],

      
        
        137
        +

      
        
        138
        +    "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],

      
        
        139
        +

      
        
        140
        +    "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],

      
        
        141
        +

      
        
        142
        +    "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],

      
        
        143
        +

      
        
        144
        +    "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],

      
        
        145
        +

      
        
        146
        +    "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],

      
        
        147
        +

      
        
        148
        +    "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],

      
        
        149
        +

      
        
        150
        +    "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],

      
        
        151
        +

      
        
        152
        +    "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],

      
        
        153
        +

      
        
        154
        +    "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],

      
        
        155
        +

      
        
        156
        +    "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],

      
        
        157
        +

      
        
        158
        +    "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],

      
        
        159
        +

      
        
        160
        +    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],

      
        
        161
        +

      
        
        162
        +    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],

      
        
        163
        +

      
        
        164
        +    "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],

      
        
        165
        +

      
        
        166
        +    "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],

      
        
        167
        +

      
        
        168
        +    "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],

      
        
        169
        +

      
        
        170
        +    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],

      
        
        171
        +

      
        
        172
        +    "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],

      
        
        173
        +

      
        
        174
        +    "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],

      
        
        175
        +

      
        
        176
        +    "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],

      
        
        177
        +

      
        
        178
        +    "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],

      
        
        179
        +

      
        
        180
        +    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],

      
        
        181
        +

      
        
        182
        +    "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="],

      
        
        183
        +

      
        
        184
        +    "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],

      
        
        185
        +

      
        
        186
        +    "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],

      
        
        187
        +

      
        
        188
        +    "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],

      
        
        189
        +

      
        
        190
        +    "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],

      
        
        191
        +

      
        
        192
        +    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],

      
        
        193
        +

      
        
        194
        +    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],

      
        
        195
        +

      
        
        196
        +    "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],

      
        
        197
        +

      
        
        198
        +    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],

      
        
        199
        +

      
        
        200
        +    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],

      
        
        201
        +

      
        
        202
        +    "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],

      
        
        203
        +

      
        
        204
        +    "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],

      
        
        205
        +

      
        
        206
        +    "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],

      
        
        207
        +

      
        
        208
        +    "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],

      
        
        209
        +

      
        
        210
        +    "elm": ["elm@0.19.1-6", "", { "optionalDependencies": { "@elm_binaries/darwin_arm64": "0.19.1-0", "@elm_binaries/darwin_x64": "0.19.1-0", "@elm_binaries/linux_x64": "0.19.1-0", "@elm_binaries/win32_x64": "0.19.1-0" }, "bin": { "elm": "bin/elm" } }, "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ=="],

      
        
        211
        +

      
        
        212
        +    "elm-land": ["elm-land@0.20.1", "", { "dependencies": { "@lydell/elm": "0.19.1-14", "chokidar": "3.5.3", "terser": "5.15.1", "typescript": "4.9.3", "vite": "5.2.8", "vite-plugin-elm-watch": "1.3.3" }, "bin": { "elm-land": "src/index.js" } }, "sha512-AY8BxYNT7mblaIO9SS2YQPdskZdMsLL6fqjAA5bORdkGIRDkMeaw+rXgiVSHUM2+TK0k/ld0TdQEAd24Moi5nw=="],

      
        
        213
        +

      
        
        214
        +    "elm-review": ["elm-review@2.13.2", "", { "dependencies": { "chalk": "^4.0.0", "chokidar": "^3.5.2", "cross-spawn": "^7.0.3", "elm-solve-deps-wasm": "^1.0.2 || ^2.0.0", "fastest-levenshtein": "^1.0.16", "find-up": "^4.1.0 || ^5.0.0", "folder-hash": "^3.3.0", "got": "^11.8.5", "graceful-fs": "^4.2.11", "minimist": "^1.2.6", "ora": "^5.4.0", "path-key": "^3.1.1", "prompts": "^2.2.1", "strip-ansi": "^6.0.0", "terminal-link": "^2.1.1", "tinyglobby": "^0.2.10", "which": "^2.0.2", "wrap-ansi": "^7.0.0" }, "bin": { "elm-review": "bin/elm-review" } }, "sha512-kI34BQ/EN1NC4KUcdZWAGNbaxWmR80kqJQRjT1ZmC0AyZRiJqdylhANucyzhPKEz60VGAkqau5axpySWXbdPLg=="],

      
        
        215
        +

      
        
        216
        +    "elm-solve-deps-wasm": ["elm-solve-deps-wasm@2.0.0", "", {}, "sha512-11OV8FgB9qsth/F94q2SJjb1MoEgbworSyNM1L+YlxVoaxp7wtWPyA8cNcPEkSoIKG1B8Tqg68ED1P6dVamHSg=="],

      
        
        217
        +

      
        
        218
        +    "elm-tooling": ["elm-tooling@1.15.1", "", { "bin": { "elm-tooling": "index.js" } }, "sha512-+rRYa7gzz6l2/Ip2i197MqkW5abOwPOP/+WlyyatLDeDhh+JR0HUMlZJYenCYodBeG/xW5xW3pRYQ2onf5bIAw=="],

      
        
        219
        +

      
        
        220
        +    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],

      
        
        221
        +

      
        
        222
        +    "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],

      
        
        223
        +

      
        
        224
        +    "esbuild": ["esbuild@0.20.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", "@esbuild/android-arm64": "0.20.2", "@esbuild/android-x64": "0.20.2", "@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-x64": "0.20.2", "@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-x64": "0.20.2", "@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-x64": "0.20.2", "@esbuild/netbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.20.2", "@esbuild/sunos-x64": "0.20.2", "@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-x64": "0.20.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g=="],

      
        
        225
        +

      
        
        226
        +    "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],

      
        
        227
        +

      
        
        228
        +    "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],

      
        
        229
        +

      
        
        230
        +    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],

      
        
        231
        +

      
        
        232
        +    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],

      
        
        233
        +

      
        
        234
        +    "folder-hash": ["folder-hash@3.3.3", "", { "dependencies": { "debug": "^4.1.1", "graceful-fs": "~4.2.0", "minimatch": "~3.0.4" }, "bin": { "folder-hash": "bin/folder-hash" } }, "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A=="],

      
        
        235
        +

      
        
        236
        +    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],

      
        
        237
        +

      
        
        238
        +    "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],

      
        
        239
        +

      
        
        240
        +    "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

      
        
        241
        +

      
        
        242
        +    "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],

      
        
        243
        +

      
        
        244
        +    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],

      
        
        245
        +

      
        
        246
        +    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],

      
        
        247
        +

      
        
        248
        +    "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],

      
        
        249
        +

      
        
        250
        +    "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],

      
        
        251
        +

      
        
        252
        +    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],

      
        
        253
        +

      
        
        254
        +    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],

      
        
        255
        +

      
        
        256
        +    "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],

      
        
        257
        +

      
        
        258
        +    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],

      
        
        259
        +

      
        
        260
        +    "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],

      
        
        261
        +

      
        
        262
        +    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],

      
        
        263
        +

      
        
        264
        +    "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],

      
        
        265
        +

      
        
        266
        +    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],

      
        
        267
        +

      
        
        268
        +    "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],

      
        
        269
        +

      
        
        270
        +    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],

      
        
        271
        +

      
        
        272
        +    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],

      
        
        273
        +

      
        
        274
        +    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],

      
        
        275
        +

      
        
        276
        +    "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],

      
        
        277
        +

      
        
        278
        +    "launch-editor": ["launch-editor@2.6.1", "", { "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" } }, "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw=="],

      
        
        279
        +

      
        
        280
        +    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],

      
        
        281
        +

      
        
        282
        +    "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],

      
        
        283
        +

      
        
        284
        +    "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],

      
        
        285
        +

      
        
        286
        +    "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],

      
        
        287
        +

      
        
        288
        +    "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],

      
        
        289
        +

      
        
        290
        +    "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="],

      
        
        291
        +

      
        
        292
        +    "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],

      
        
        293
        +

      
        
        294
        +    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

      
        
        295
        +

      
        
        296
        +    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],

      
        
        297
        +

      
        
        298
        +    "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],

      
        
        299
        +

      
        
        300
        +    "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],

      
        
        301
        +

      
        
        302
        +    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],

      
        
        303
        +

      
        
        304
        +    "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],

      
        
        305
        +

      
        
        306
        +    "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],

      
        
        307
        +

      
        
        308
        +    "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],

      
        
        309
        +

      
        
        310
        +    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],

      
        
        311
        +

      
        
        312
        +    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],

      
        
        313
        +

      
        
        314
        +    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],

      
        
        315
        +

      
        
        316
        +    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],

      
        
        317
        +

      
        
        318
        +    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],

      
        
        319
        +

      
        
        320
        +    "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],

      
        
        321
        +

      
        
        322
        +    "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],

      
        
        323
        +

      
        
        324
        +    "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],

      
        
        325
        +

      
        
        326
        +    "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],

      
        
        327
        +

      
        
        328
        +    "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],

      
        
        329
        +

      
        
        330
        +    "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],

      
        
        331
        +

      
        
        332
        +    "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],

      
        
        333
        +

      
        
        334
        +    "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],

      
        
        335
        +

      
        
        336
        +    "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],

      
        
        337
        +

      
        
        338
        +    "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],

      
        
        339
        +

      
        
        340
        +    "rollup": ["rollup@4.43.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.43.0", "@rollup/rollup-android-arm64": "4.43.0", "@rollup/rollup-darwin-arm64": "4.43.0", "@rollup/rollup-darwin-x64": "4.43.0", "@rollup/rollup-freebsd-arm64": "4.43.0", "@rollup/rollup-freebsd-x64": "4.43.0", "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", "@rollup/rollup-linux-arm-musleabihf": "4.43.0", "@rollup/rollup-linux-arm64-gnu": "4.43.0", "@rollup/rollup-linux-arm64-musl": "4.43.0", "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-musl": "4.43.0", "@rollup/rollup-linux-s390x-gnu": "4.43.0", "@rollup/rollup-linux-x64-gnu": "4.43.0", "@rollup/rollup-linux-x64-musl": "4.43.0", "@rollup/rollup-win32-arm64-msvc": "4.43.0", "@rollup/rollup-win32-ia32-msvc": "4.43.0", "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg=="],

      
        
        341
        +

      
        
        342
        +    "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],

      
        
        343
        +

      
        
        344
        +    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],

      
        
        345
        +

      
        
        346
        +    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],

      
        
        347
        +

      
        
        348
        +    "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],

      
        
        349
        +

      
        
        350
        +    "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],

      
        
        351
        +

      
        
        352
        +    "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],

      
        
        353
        +

      
        
        354
        +    "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],

      
        
        355
        +

      
        
        356
        +    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],

      
        
        357
        +

      
        
        358
        +    "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],

      
        
        359
        +

      
        
        360
        +    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],

      
        
        361
        +

      
        
        362
        +    "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],

      
        
        363
        +

      
        
        364
        +    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],

      
        
        365
        +

      
        
        366
        +    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],

      
        
        367
        +

      
        
        368
        +    "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],

      
        
        369
        +

      
        
        370
        +    "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="],

      
        
        371
        +

      
        
        372
        +    "terser": ["terser@5.15.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw=="],

      
        
        373
        +

      
        
        374
        +    "tiny-decoders": ["tiny-decoders@7.0.1", "", {}, "sha512-P1LaHTLASl/lCrdtwgAAVwxt4bEAPmxpf9HMQrlCkAseaT8oH8oxm8ndy4nx5rLTcL5U/Qxp1a+FDoQfS/ZgQQ=="],

      
        
        375
        +

      
        
        376
        +    "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],

      
        
        377
        +

      
        
        378
        +    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],

      
        
        379
        +

      
        
        380
        +    "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],

      
        
        381
        +

      
        
        382
        +    "typescript": ["typescript@4.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA=="],

      
        
        383
        +

      
        
        384
        +    "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],

      
        
        385
        +

      
        
        386
        +    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],

      
        
        387
        +

      
        
        388
        +    "vite": ["vite@5.2.8", "", { "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", "rollup": "^4.13.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA=="],

      
        
        389
        +

      
        
        390
        +    "vite-plugin-elm-watch": ["vite-plugin-elm-watch@1.3.3", "", { "dependencies": { "cross-spawn": "7.0.3", "elm": "0.19.1-6", "launch-editor": "2.6.1", "terser": "5.26.0", "tiny-decoders": "7.0.1" } }, "sha512-rR78gmeYp08E4CvtpnZuumsmNXaG/XOJ/xtOVfEG3TOJqDChXy1DzE1pnPsERohbELkov7ZZPHRLGV1z2iyxKg=="],

      
        
        391
        +

      
        
        392
        +    "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],

      
        
        393
        +

      
        
        394
        +    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],

      
        
        395
        +

      
        
        396
        +    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],

      
        
        397
        +

      
        
        398
        +    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],

      
        
        399
        +

      
        
        400
        +    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],

      
        
        401
        +

      
        
        402
        +    "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

      
        
        403
        +

      
        
        404
        +    "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],

      
        
        405
        +

      
        
        406
        +    "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

      
        
        407
        +

      
        
        408
        +    "vite-plugin-elm-watch/cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],

      
        
        409
        +

      
        
        410
        +    "vite-plugin-elm-watch/terser": ["terser@5.26.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ=="],

      
        
        411
        +  }

      
        
        412
        +}

      
A web/elm-land.json
···
        
        1
        +{

      
        
        2
        +  "app": {

      
        
        3
        +    "elm": {

      
        
        4
        +      "development": {

      
        
        5
        +        "debugger": true

      
        
        6
        +      },

      
        
        7
        +      "production": {

      
        
        8
        +        "debugger": false

      
        
        9
        +      }

      
        
        10
        +    },

      
        
        11
        +    "env": [],

      
        
        12
        +    "html": {

      
        
        13
        +      "attributes": {

      
        
        14
        +        "html": {

      
        
        15
        +          "lang": "en"

      
        
        16
        +        },

      
        
        17
        +        "head": {}

      
        
        18
        +      },

      
        
        19
        +      "title": "Onasty",

      
        
        20
        +      "meta": [

      
        
        21
        +        {

      
        
        22
        +          "charset": "UTF-8"

      
        
        23
        +        },

      
        
        24
        +        {

      
        
        25
        +          "http-equiv": "X-UA-Compatible",

      
        
        26
        +          "content": "IE=edge"

      
        
        27
        +        },

      
        
        28
        +        {

      
        
        29
        +          "name": "viewport",

      
        
        30
        +          "content": "width=device-width, initial-scale=1.0"

      
        
        31
        +        }

      
        
        32
        +      ],

      
        
        33
        +      "link": [],

      
        
        34
        +      "script": []

      
        
        35
        +    },

      
        
        36
        +    "router": {

      
        
        37
        +      "useHashRouting": false

      
        
        38
        +    },

      
        
        39
        +    "proxy": {

      
        
        40
        +      "/api": "http://localhost:8000"

      
        
        41
        +    }

      
        
        42
        +  }

      
        
        43
        +}

      
A web/elm-tooling.json
···
        
        1
        +{

      
        
        2
        +    "tools": {

      
        
        3
        +        "elm": "0.19.1",

      
        
        4
        +        "elm-format": "0.8.7",

      
        
        5
        +        "elm-test-rs": "3.0.0"

      
        
        6
        +    }

      
        
        7
        +}

      
A web/elm.json
···
        
        1
        +{

      
        
        2
        +    "type": "application",

      
        
        3
        +    "source-directories": [

      
        
        4
        +        "src",

      
        
        5
        +        ".elm-land/src"

      
        
        6
        +    ],

      
        
        7
        +    "elm-version": "0.19.1",

      
        
        8
        +    "dependencies": {

      
        
        9
        +        "direct": {

      
        
        10
        +            "elm/browser": "1.0.2",

      
        
        11
        +            "elm/core": "1.0.5",

      
        
        12
        +            "elm/html": "1.0.0",

      
        
        13
        +            "elm/http": "2.0.0",

      
        
        14
        +            "elm/json": "1.1.3",

      
        
        15
        +            "elm/time": "1.0.0",

      
        
        16
        +            "elm/url": "1.0.0",

      
        
        17
        +            "simonh1000/elm-jwt": "7.1.1"

      
        
        18
        +        },

      
        
        19
        +        "indirect": {

      
        
        20
        +            "danfishgold/base64-bytes": "1.1.0",

      
        
        21
        +            "elm/bytes": "1.0.8",

      
        
        22
        +            "elm/file": "1.0.5",

      
        
        23
        +            "elm/virtual-dom": "1.0.3"

      
        
        24
        +        }

      
        
        25
        +    },

      
        
        26
        +    "test-dependencies": {

      
        
        27
        +        "direct": {

      
        
        28
        +            "elm-explorations/test": "2.2.0"

      
        
        29
        +        },

      
        
        30
        +        "indirect": {

      
        
        31
        +            "elm/random": "1.0.0"

      
        
        32
        +        }

      
        
        33
        +    }

      
        
        34
        +}

      
A web/package.json
···
        
        1
        +{

      
        
        2
        +  "private": true,

      
        
        3
        +  "scripts": {

      
        
        4
        +    "postinstall": "elm-tooling install"

      
        
        5
        +  },

      
        
        6
        +  "devDependencies": {

      
        
        7
        +    "elm-land": "^0.20.1",

      
        
        8
        +    "elm-review": "^2.13.2",

      
        
        9
        +    "elm-tooling": "^1.15.1"

      
        
        10
        +  }

      
        
        11
        +}

      
A web/review/elm.json
···
        
        1
        +{

      
        
        2
        +    "type": "application",

      
        
        3
        +    "source-directories": [

      
        
        4
        +        "src"

      
        
        5
        +    ],

      
        
        6
        +    "elm-version": "0.19.1",

      
        
        7
        +    "dependencies": {

      
        
        8
        +        "direct": {

      
        
        9
        +            "elm/core": "1.0.5",

      
        
        10
        +            "elm/json": "1.1.3",

      
        
        11
        +            "elm/project-metadata-utils": "1.0.2",

      
        
        12
        +            "jfmengels/elm-review": "2.15.1",

      
        
        13
        +            "jfmengels/elm-review-code-style": "1.2.0",

      
        
        14
        +            "jfmengels/elm-review-common": "1.3.3",

      
        
        15
        +            "jfmengels/elm-review-debug": "1.0.8",

      
        
        16
        +            "jfmengels/elm-review-documentation": "2.0.4",

      
        
        17
        +            "jfmengels/elm-review-simplify": "2.1.8",

      
        
        18
        +            "jfmengels/elm-review-unused": "1.2.4",

      
        
        19
        +            "stil4m/elm-syntax": "7.3.9"

      
        
        20
        +        },

      
        
        21
        +        "indirect": {

      
        
        22
        +            "elm/bytes": "1.0.8",

      
        
        23
        +            "elm/html": "1.0.0",

      
        
        24
        +            "elm/parser": "1.1.0",

      
        
        25
        +            "elm/random": "1.0.0",

      
        
        26
        +            "elm/regex": "1.0.0",

      
        
        27
        +            "elm/time": "1.0.0",

      
        
        28
        +            "elm/virtual-dom": "1.0.3",

      
        
        29
        +            "elm-explorations/test": "2.2.0",

      
        
        30
        +            "pzp1997/assoc-list": "1.0.0",

      
        
        31
        +            "rtfeldman/elm-hex": "1.0.0",

      
        
        32
        +            "stil4m/structured-writer": "1.0.3"

      
        
        33
        +        }

      
        
        34
        +    },

      
        
        35
        +    "test-dependencies": {

      
        
        36
        +        "direct": {

      
        
        37
        +            "elm-explorations/test": "2.2.0"

      
        
        38
        +        },

      
        
        39
        +        "indirect": {}

      
        
        40
        +    }

      
        
        41
        +}
      
A web/review/src/ReviewConfig.elm
···
        
        1
        +module ReviewConfig exposing (config)

      
        
        2
        +

      
        
        3
        +import Docs.ReviewAtDocs

      
        
        4
        +import NoConfusingPrefixOperator

      
        
        5
        +import NoDebug.Log

      
        
        6
        +import NoDebug.TodoOrToString

      
        
        7
        +import NoExposingEverything

      
        
        8
        +import NoImportingEverything

      
        
        9
        +import NoMissingTypeAnnotation

      
        
        10
        +import NoMissingTypeAnnotationInLetIn

      
        
        11
        +import NoMissingTypeExpose

      
        
        12
        +import NoPrematureLetComputation

      
        
        13
        +import NoSimpleLetBody

      
        
        14
        +import NoUnused.CustomTypeConstructorArgs

      
        
        15
        +import NoUnused.CustomTypeConstructors

      
        
        16
        +import NoUnused.Dependencies

      
        
        17
        +import NoUnused.Exports

      
        
        18
        +import NoUnused.Parameters

      
        
        19
        +import NoUnused.Patterns

      
        
        20
        +import NoUnused.Variables

      
        
        21
        +import Review.Rule as Rule exposing (Rule)

      
        
        22
        +import Simplify

      
        
        23
        +

      
        
        24
        +

      
        
        25
        +config : List Rule

      
        
        26
        +config =

      
        
        27
        +    [ Docs.ReviewAtDocs.rule

      
        
        28
        +    , NoConfusingPrefixOperator.rule

      
        
        29
        +    , NoDebug.Log.rule

      
        
        30
        +    , NoDebug.TodoOrToString.rule

      
        
        31
        +        |> Rule.ignoreErrorsForDirectories [ "tests/" ]

      
        
        32
        +    , NoExposingEverything.rule

      
        
        33
        +    , NoImportingEverything.rule []

      
        
        34
        +    , NoMissingTypeAnnotation.rule

      
        
        35
        +    , NoMissingTypeAnnotationInLetIn.rule

      
        
        36
        +    , NoMissingTypeExpose.rule

      
        
        37
        +    , NoSimpleLetBody.rule

      
        
        38
        +    , NoPrematureLetComputation.rule

      
        
        39
        +    , NoUnused.CustomTypeConstructors.rule []

      
        
        40
        +    , NoUnused.CustomTypeConstructorArgs.rule

      
        
        41
        +    , NoUnused.Dependencies.rule

      
        
        42
        +    , NoUnused.Exports.rule

      
        
        43
        +        |> Rule.ignoreErrorsForFiles [ "src/Effect.elm" ]

      
        
        44
        +    , NoUnused.Parameters.rule

      
        
        45
        +    , NoUnused.Patterns.rule

      
        
        46
        +    , NoUnused.Variables.rule

      
        
        47
        +    , Simplify.rule Simplify.defaults

      
        
        48
        +    ]

      
A web/src/Api.elm
···
        
        1
        +module Api exposing (HttpRequestDetails, Response(..), errorToFriendlyMessage)

      
        
        2
        +

      
        
        3
        +import Http

      
        
        4
        +import Json.Decode

      
        
        5
        +

      
        
        6
        +

      
        
        7
        +type Response value

      
        
        8
        +    = Loading

      
        
        9
        +    | Success value

      
        
        10
        +    | Failure Http.Error

      
        
        11
        +

      
        
        12
        +

      
        
        13
        +type alias HttpRequestDetails msg =

      
        
        14
        +    { endpoint : String

      
        
        15
        +    , method : String

      
        
        16
        +    , body : Http.Body

      
        
        17
        +    , decoder : Json.Decode.Decoder msg

      
        
        18
        +    , onHttpError : Http.Error -> msg

      
        
        19
        +    }

      
        
        20
        +

      
        
        21
        +

      
        
        22
        +errorToFriendlyMessage : Http.Error -> String

      
        
        23
        +errorToFriendlyMessage httpError =

      
        
        24
        +    case httpError of

      
        
        25
        +        Http.BadUrl _ ->

      
        
        26
        +            "This page requested a bad URL"

      
        
        27
        +

      
        
        28
        +        Http.Timeout ->

      
        
        29
        +            "Request took too long to respond"

      
        
        30
        +

      
        
        31
        +        Http.NetworkError ->

      
        
        32
        +            "Could not connect to the API"

      
        
        33
        +

      
        
        34
        +        Http.BadStatus code ->

      
        
        35
        +            case code of

      
        
        36
        +                404 ->

      
        
        37
        +                    "Not found"

      
        
        38
        +

      
        
        39
        +                401 ->

      
        
        40
        +                    "Unauthorized"

      
        
        41
        +

      
        
        42
        +                _ ->

      
        
        43
        +                    "API returned an error code"

      
        
        44
        +

      
        
        45
        +        Http.BadBody _ ->

      
        
        46
        +            "Unexpected response from API"

      
A web/src/Api/Auth.elm
···
        
        1
        +module Api.Auth exposing (refreshToken, signin)

      
        
        2
        +

      
        
        3
        +import Data.Credentials as Credentials exposing (Credentials)

      
        
        4
        +import Effect exposing (Effect)

      
        
        5
        +import Http

      
        
        6
        +import Json.Encode as Encode

      
        
        7
        +

      
        
        8
        +

      
        
        9
        +signin :

      
        
        10
        +    { onResponse : Result Http.Error Credentials -> msg

      
        
        11
        +    , email : String

      
        
        12
        +    , password : String

      
        
        13
        +    }

      
        
        14
        +    -> Effect msg

      
        
        15
        +signin options =

      
        
        16
        +    let

      
        
        17
        +        body : Encode.Value

      
        
        18
        +        body =

      
        
        19
        +            Encode.object

      
        
        20
        +                [ ( "email", Encode.string options.email )

      
        
        21
        +                , ( "password", Encode.string options.password )

      
        
        22
        +                ]

      
        
        23
        +    in

      
        
        24
        +    Effect.sendApiRequest

      
        
        25
        +        { endpoint = "/api/v1/auth/signin"

      
        
        26
        +        , method = "POST"

      
        
        27
        +        , body = Http.jsonBody body

      
        
        28
        +        , onResponse = options.onResponse

      
        
        29
        +        , decoder = Credentials.decode

      
        
        30
        +        }

      
        
        31
        +

      
        
        32
        +

      
        
        33
        +refreshToken :

      
        
        34
        +    { onResponse : Result Http.Error Credentials -> msg

      
        
        35
        +    , refreshToken : String

      
        
        36
        +    }

      
        
        37
        +    -> Effect msg

      
        
        38
        +refreshToken options =

      
        
        39
        +    let

      
        
        40
        +        body : Encode.Value

      
        
        41
        +        body =

      
        
        42
        +            Encode.object

      
        
        43
        +                [ ( "refresh_token", Encode.string options.refreshToken ) ]

      
        
        44
        +    in

      
        
        45
        +    Effect.sendApiRequest

      
        
        46
        +        { endpoint = "/api/v1/auth/refresh-tokens"

      
        
        47
        +        , method = "POST"

      
        
        48
        +        , body = Http.jsonBody body

      
        
        49
        +        , onResponse = options.onResponse

      
        
        50
        +        , decoder = Credentials.decode

      
        
        51
        +        }

      
A web/src/Api/Me.elm
···
        
        1
        +module Api.Me exposing (get)

      
        
        2
        +

      
        
        3
        +import Data.Me as Me exposing (Me)

      
        
        4
        +import Effect exposing (Effect)

      
        
        5
        +import Http

      
        
        6
        +

      
        
        7
        +

      
        
        8
        +get : { onResponse : Result Http.Error Me -> msg } -> Effect msg

      
        
        9
        +get options =

      
        
        10
        +    Effect.sendApiRequest

      
        
        11
        +        { endpoint = "/api/v1/me"

      
        
        12
        +        , method = "GET"

      
        
        13
        +        , body = Http.emptyBody

      
        
        14
        +        , onResponse = options.onResponse

      
        
        15
        +        , decoder = Me.decode

      
        
        16
        +        }

      
A web/src/Auth.elm
···
        
        1
        +module Auth exposing (User, onPageLoad, viewCustomPage)

      
        
        2
        +

      
        
        3
        +import Auth.Action

      
        
        4
        +import Dict

      
        
        5
        +import Route exposing (Route)

      
        
        6
        +import Route.Path

      
        
        7
        +import Shared

      
        
        8
        +import View exposing (View)

      
        
        9
        +

      
        
        10
        +

      
        
        11
        +type alias User =

      
        
        12
        +    { accessToken : String

      
        
        13
        +    , refreshToken : String

      
        
        14
        +    }

      
        
        15
        +

      
        
        16
        +

      
        
        17
        +{-| Called before an auth-only page is loaded.

      
        
        18
        +-}

      
        
        19
        +onPageLoad : Shared.Model -> Route () -> Auth.Action.Action User

      
        
        20
        +onPageLoad shared _ =

      
        
        21
        +    case shared.credentials of

      
        
        22
        +        Just credentials ->

      
        
        23
        +            Auth.Action.loadPageWithUser

      
        
        24
        +                { accessToken = credentials.accessToken

      
        
        25
        +                , refreshToken = credentials.refreshToken

      
        
        26
        +                }

      
        
        27
        +

      
        
        28
        +        _ ->

      
        
        29
        +            Auth.Action.pushRoute

      
        
        30
        +                { path = Route.Path.SignIn

      
        
        31
        +                , query = Dict.empty

      
        
        32
        +                , hash = Nothing

      
        
        33
        +                }

      
        
        34
        +

      
        
        35
        +

      
        
        36
        +{-| Renders whenever `Auth.Action.loadCustomPage` is returned from `onPageLoad`.

      
        
        37
        +-}

      
        
        38
        +viewCustomPage : Shared.Model -> Route () -> View Never

      
        
        39
        +viewCustomPage _ _ =

      
        
        40
        +    View.fromString "Loading..."

      
A web/src/Data/Credentials.elm
···
        
        1
        +module Data.Credentials exposing

      
        
        2
        +    ( Credentials

      
        
        3
        +    , decode

      
        
        4
        +    )

      
        
        5
        +

      
        
        6
        +{-|

      
        
        7
        +

      
        
        8
        +@docs Credentials

      
        
        9
        +@docs decode

      
        
        10
        +

      
        
        11
        +-}

      
        
        12
        +

      
        
        13
        +import Json.Decode as Decode exposing (Decoder)

      
        
        14
        +

      
        
        15
        +

      
        
        16
        +type alias Credentials =

      
        
        17
        +    { accessToken : String

      
        
        18
        +    , refreshToken : String

      
        
        19
        +    }

      
        
        20
        +

      
        
        21
        +

      
        
        22
        +decode : Decoder Credentials

      
        
        23
        +decode =

      
        
        24
        +    Decode.map2 Credentials

      
        
        25
        +        (Decode.field "access_token" Decode.string)

      
        
        26
        +        (Decode.field "refresh_token" Decode.string)

      
A web/src/Data/Me.elm
···
        
        1
        +module Data.Me exposing (Me, decode)

      
        
        2
        +

      
        
        3
        +import Json.Decode as Decode exposing (Decoder)

      
        
        4
        +

      
        
        5
        +

      
        
        6
        +type alias Me =

      
        
        7
        +    { email : String

      
        
        8
        +    , createdAt : String -- TODO: upgrade to elm/time

      
        
        9
        +    }

      
        
        10
        +

      
        
        11
        +

      
        
        12
        +decode : Decoder Me

      
        
        13
        +decode =

      
        
        14
        +    Decode.map2 Me

      
        
        15
        +        (Decode.field "email" Decode.string)

      
        
        16
        +        (Decode.field "created_at" Decode.string)

      
A web/src/Effect.elm
···
        
        1
        +module Effect exposing

      
        
        2
        +    ( Effect

      
        
        3
        +    , none, batch

      
        
        4
        +    , sendCmd, sendMsg

      
        
        5
        +    , pushRoute, replaceRoute

      
        
        6
        +    , pushRoutePath, replaceRoutePath

      
        
        7
        +    , loadExternalUrl, back

      
        
        8
        +    , sendApiRequest

      
        
        9
        +    , signin, logout, saveUser, clearUser

      
        
        10
        +    , map, toCmd, refreshTokens

      
        
        11
        +    )

      
        
        12
        +

      
        
        13
        +{-|

      
        
        14
        +

      
        
        15
        +@docs Effect

      
        
        16
        +

      
        
        17
        +@docs none, batch

      
        
        18
        +@docs sendCmd, sendMsg

      
        
        19
        +

      
        
        20
        +@docs pushRoute, replaceRoute

      
        
        21
        +@docs pushRoutePath, replaceRoutePath

      
        
        22
        +@docs loadExternalUrl, back

      
        
        23
        +

      
        
        24
        +@docs sendApiRequest, refreshTokens

      
        
        25
        +@docs signin, logout, saveUser, clearUser

      
        
        26
        +

      
        
        27
        +@docs map, toCmd

      
        
        28
        +

      
        
        29
        +-}

      
        
        30
        +

      
        
        31
        +import Api exposing (HttpRequestDetails)

      
        
        32
        +import Browser.Navigation

      
        
        33
        +import Data.Credentials exposing (Credentials)

      
        
        34
        +import Dict exposing (Dict)

      
        
        35
        +import Http

      
        
        36
        +import Json.Decode

      
        
        37
        +import Json.Encode

      
        
        38
        +import Ports exposing (sendToLocalStorage)

      
        
        39
        +import Route

      
        
        40
        +import Route.Path

      
        
        41
        +import Shared.Model

      
        
        42
        +import Shared.Msg

      
        
        43
        +import Task

      
        
        44
        +import Url exposing (Url)

      
        
        45
        +

      
        
        46
        +

      
        
        47
        +type Effect msg

      
        
        48
        +    = -- BASICS

      
        
        49
        +      None

      
        
        50
        +    | Batch (List (Effect msg))

      
        
        51
        +    | SendCmd (Cmd msg)

      
        
        52
        +      -- ROUTING

      
        
        53
        +    | PushUrl String

      
        
        54
        +    | ReplaceUrl String

      
        
        55
        +    | LoadExternalUrl String

      
        
        56
        +    | Back

      
        
        57
        +      -- SHARED

      
        
        58
        +    | SendSharedMsg Shared.Msg.Msg

      
        
        59
        +    | SendToLocalStorage { key : String, value : Json.Encode.Value }

      
        
        60
        +    | SendApiRequest (HttpRequestDetails msg)

      
        
        61
        +

      
        
        62
        +

      
        
        63
        +

      
        
        64
        +-- BASICS

      
        
        65
        +

      
        
        66
        +

      
        
        67
        +{-| Don't send any effect.

      
        
        68
        +-}

      
        
        69
        +none : Effect msg

      
        
        70
        +none =

      
        
        71
        +    None

      
        
        72
        +

      
        
        73
        +

      
        
        74
        +{-| Send multiple effects at once.

      
        
        75
        +-}

      
        
        76
        +batch : List (Effect msg) -> Effect msg

      
        
        77
        +batch =

      
        
        78
        +    Batch

      
        
        79
        +

      
        
        80
        +

      
        
        81
        +{-| Send a normal `Cmd msg` as an effect, something like `Http.get` or `Random.generate`.

      
        
        82
        +-}

      
        
        83
        +sendCmd : Cmd msg -> Effect msg

      
        
        84
        +sendCmd =

      
        
        85
        +    SendCmd

      
        
        86
        +

      
        
        87
        +

      
        
        88
        +{-| Send a message as an effect. Useful when emitting events from UI components.

      
        
        89
        +-}

      
        
        90
        +sendMsg : msg -> Effect msg

      
        
        91
        +sendMsg msg =

      
        
        92
        +    Task.succeed msg

      
        
        93
        +        |> Task.perform identity

      
        
        94
        +        |> SendCmd

      
        
        95
        +

      
        
        96
        +

      
        
        97
        +

      
        
        98
        +-- ROUTING

      
        
        99
        +

      
        
        100
        +

      
        
        101
        +{-| Set the new route, and make the back button go back to the current route.

      
        
        102
        +-}

      
        
        103
        +pushRoute :

      
        
        104
        +    { path : Route.Path.Path

      
        
        105
        +    , query : Dict String String

      
        
        106
        +    , hash : Maybe String

      
        
        107
        +    }

      
        
        108
        +    -> Effect msg

      
        
        109
        +pushRoute route =

      
        
        110
        +    PushUrl (Route.toString route)

      
        
        111
        +

      
        
        112
        +

      
        
        113
        +{-| Same as `Effect.pushRoute`, but without `query` or `hash` support

      
        
        114
        +-}

      
        
        115
        +pushRoutePath : Route.Path.Path -> Effect msg

      
        
        116
        +pushRoutePath path =

      
        
        117
        +    PushUrl (Route.Path.toString path)

      
        
        118
        +

      
        
        119
        +

      
        
        120
        +{-| Set the new route, but replace the previous one, so clicking the back

      
        
        121
        +button **won't** go back to the previous route.

      
        
        122
        +-}

      
        
        123
        +replaceRoute :

      
        
        124
        +    { path : Route.Path.Path

      
        
        125
        +    , query : Dict String String

      
        
        126
        +    , hash : Maybe String

      
        
        127
        +    }

      
        
        128
        +    -> Effect msg

      
        
        129
        +replaceRoute route =

      
        
        130
        +    ReplaceUrl (Route.toString route)

      
        
        131
        +

      
        
        132
        +

      
        
        133
        +{-| Same as `Effect.replaceRoute`, but without `query` or `hash` support

      
        
        134
        +-}

      
        
        135
        +replaceRoutePath : Route.Path.Path -> Effect msg

      
        
        136
        +replaceRoutePath path =

      
        
        137
        +    ReplaceUrl (Route.Path.toString path)

      
        
        138
        +

      
        
        139
        +

      
        
        140
        +{-| Redirect users to a new URL, somewhere external to your web application.

      
        
        141
        +-}

      
        
        142
        +loadExternalUrl : String -> Effect msg

      
        
        143
        +loadExternalUrl =

      
        
        144
        +    LoadExternalUrl

      
        
        145
        +

      
        
        146
        +

      
        
        147
        +{-| Navigate back one page

      
        
        148
        +-}

      
        
        149
        +back : Effect msg

      
        
        150
        +back =

      
        
        151
        +    Back

      
        
        152
        +

      
        
        153
        +

      
        
        154
        +

      
        
        155
        +-- SHARED

      
        
        156
        +

      
        
        157
        +

      
        
        158
        +sendApiRequest :

      
        
        159
        +    { endpoint : String

      
        
        160
        +    , method : String

      
        
        161
        +    , body : Http.Body

      
        
        162
        +    , decoder : Json.Decode.Decoder value

      
        
        163
        +    , onResponse : Result Http.Error value -> msg

      
        
        164
        +    }

      
        
        165
        +    -> Effect msg

      
        
        166
        +sendApiRequest opts =

      
        
        167
        +    let

      
        
        168
        +        onHttpError : Http.Error -> msg

      
        
        169
        +        onHttpError httpError =

      
        
        170
        +            opts.onResponse (Err httpError)

      
        
        171
        +

      
        
        172
        +        decoder : Json.Decode.Decoder msg

      
        
        173
        +        decoder =

      
        
        174
        +            opts.decoder

      
        
        175
        +                |> Json.Decode.map Ok

      
        
        176
        +                |> Json.Decode.map opts.onResponse

      
        
        177
        +    in

      
        
        178
        +    SendApiRequest

      
        
        179
        +        { endpoint = opts.endpoint

      
        
        180
        +        , method = opts.method

      
        
        181
        +        , body = opts.body

      
        
        182
        +        , onHttpError = onHttpError

      
        
        183
        +        , decoder = decoder

      
        
        184
        +        }

      
        
        185
        +

      
        
        186
        +

      
        
        187
        +refreshTokens : Effect msg

      
        
        188
        +refreshTokens =

      
        
        189
        +    SendSharedMsg Shared.Msg.TriggerTokenRefresh

      
        
        190
        +

      
        
        191
        +

      
        
        192
        +signin : Credentials -> Effect msg

      
        
        193
        +signin credentials =

      
        
        194
        +    SendSharedMsg (Shared.Msg.SignedIn credentials)

      
        
        195
        +

      
        
        196
        +

      
        
        197
        +logout : Effect msg

      
        
        198
        +logout =

      
        
        199
        +    SendSharedMsg Shared.Msg.Logout

      
        
        200
        +

      
        
        201
        +

      
        
        202
        +saveUser : String -> String -> Effect msg

      
        
        203
        +saveUser accessToken refreshToken =

      
        
        204
        +    batch

      
        
        205
        +        [ SendToLocalStorage { key = "access_token", value = Json.Encode.string accessToken }

      
        
        206
        +        , SendToLocalStorage { key = "refresh_token", value = Json.Encode.string refreshToken }

      
        
        207
        +        ]

      
        
        208
        +

      
        
        209
        +

      
        
        210
        +clearUser : Effect msg

      
        
        211
        +clearUser =

      
        
        212
        +    batch

      
        
        213
        +        [ SendToLocalStorage { key = "access_token", value = Json.Encode.null }

      
        
        214
        +        , SendToLocalStorage { key = "refresh_token", value = Json.Encode.null }

      
        
        215
        +        ]

      
        
        216
        +

      
        
        217
        +

      
        
        218
        +

      
        
        219
        +-- INTERNALS

      
        
        220
        +

      
        
        221
        +

      
        
        222
        +{-| Elm Land depends on this function to connect pages and layouts

      
        
        223
        +together into the overall app.

      
        
        224
        +-}

      
        
        225
        +map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2

      
        
        226
        +map fn effect =

      
        
        227
        +    case effect of

      
        
        228
        +        None ->

      
        
        229
        +            None

      
        
        230
        +

      
        
        231
        +        Batch list ->

      
        
        232
        +            Batch (List.map (map fn) list)

      
        
        233
        +

      
        
        234
        +        SendCmd cmd ->

      
        
        235
        +            SendCmd (Cmd.map fn cmd)

      
        
        236
        +

      
        
        237
        +        PushUrl url ->

      
        
        238
        +            PushUrl url

      
        
        239
        +

      
        
        240
        +        ReplaceUrl url ->

      
        
        241
        +            ReplaceUrl url

      
        
        242
        +

      
        
        243
        +        Back ->

      
        
        244
        +            Back

      
        
        245
        +

      
        
        246
        +        LoadExternalUrl url ->

      
        
        247
        +            LoadExternalUrl url

      
        
        248
        +

      
        
        249
        +        SendSharedMsg sharedMsg ->

      
        
        250
        +            SendSharedMsg sharedMsg

      
        
        251
        +

      
        
        252
        +        SendToLocalStorage options ->

      
        
        253
        +            SendToLocalStorage options

      
        
        254
        +

      
        
        255
        +        SendApiRequest opts ->

      
        
        256
        +            SendApiRequest

      
        
        257
        +                { endpoint = opts.endpoint

      
        
        258
        +                , method = opts.method

      
        
        259
        +                , body = opts.body

      
        
        260
        +                , decoder = Json.Decode.map fn opts.decoder

      
        
        261
        +                , onHttpError = \err -> fn (opts.onHttpError err)

      
        
        262
        +                }

      
        
        263
        +

      
        
        264
        +

      
        
        265
        +{-| Elm Land depends on this function to perform your effects.

      
        
        266
        +-}

      
        
        267
        +toCmd :

      
        
        268
        +    { key : Browser.Navigation.Key

      
        
        269
        +    , url : Url

      
        
        270
        +    , shared : Shared.Model.Model

      
        
        271
        +    , fromSharedMsg : Shared.Msg.Msg -> msg

      
        
        272
        +    , batch : List msg -> msg

      
        
        273
        +    , toCmd : msg -> Cmd msg

      
        
        274
        +    }

      
        
        275
        +    -> Effect msg

      
        
        276
        +    -> Cmd msg

      
        
        277
        +toCmd options effect =

      
        
        278
        +    case effect of

      
        
        279
        +        None ->

      
        
        280
        +            Cmd.none

      
        
        281
        +

      
        
        282
        +        Batch list ->

      
        
        283
        +            Cmd.batch (List.map (toCmd options) list)

      
        
        284
        +

      
        
        285
        +        SendCmd cmd ->

      
        
        286
        +            cmd

      
        
        287
        +

      
        
        288
        +        PushUrl url ->

      
        
        289
        +            Browser.Navigation.pushUrl options.key url

      
        
        290
        +

      
        
        291
        +        ReplaceUrl url ->

      
        
        292
        +            Browser.Navigation.replaceUrl options.key url

      
        
        293
        +

      
        
        294
        +        Back ->

      
        
        295
        +            Browser.Navigation.back options.key 1

      
        
        296
        +

      
        
        297
        +        LoadExternalUrl url ->

      
        
        298
        +            Browser.Navigation.load url

      
        
        299
        +

      
        
        300
        +        SendSharedMsg sharedMsg ->

      
        
        301
        +            Task.succeed sharedMsg

      
        
        302
        +                |> Task.perform options.fromSharedMsg

      
        
        303
        +

      
        
        304
        +        SendToLocalStorage opts ->

      
        
        305
        +            sendToLocalStorage opts

      
        
        306
        +

      
        
        307
        +        SendApiRequest opts ->

      
        
        308
        +            let

      
        
        309
        +                headers : List Http.Header

      
        
        310
        +                headers =

      
        
        311
        +                    case options.shared.credentials of

      
        
        312
        +                        Just tok ->

      
        
        313
        +                            if not (String.contains opts.endpoint "refresh-tokens") then

      
        
        314
        +                                [ Http.header "Authorization" ("Bearer " ++ tok.accessToken) ]

      
        
        315
        +

      
        
        316
        +                            else

      
        
        317
        +                                []

      
        
        318
        +

      
        
        319
        +                        Nothing ->

      
        
        320
        +                            []

      
        
        321
        +            in

      
        
        322
        +            Http.request

      
        
        323
        +                { method = opts.method

      
        
        324
        +                , url = opts.endpoint

      
        
        325
        +                , headers = headers

      
        
        326
        +                , body = opts.body

      
        
        327
        +                , expect =

      
        
        328
        +                    Http.expectJson

      
        
        329
        +                        (\httpResult ->

      
        
        330
        +                            case httpResult of

      
        
        331
        +                                Ok msg ->

      
        
        332
        +                                    msg

      
        
        333
        +

      
        
        334
        +                                Err err ->

      
        
        335
        +                                    opts.onHttpError err

      
        
        336
        +                        )

      
        
        337
        +                        opts.decoder

      
        
        338
        +                , timeout = Just (1000 * 60) -- 60 second timeout

      
        
        339
        +                , tracker = Nothing

      
        
        340
        +                }

      
A web/src/JwtUtil.elm
···
        
        1
        +module JwtUtil exposing (isExpired)

      
        
        2
        +

      
        
        3
        +import Jwt

      
        
        4
        +import Time

      
        
        5
        +

      
        
        6
        +

      
        
        7
        +{-| Checks if a JWT token is expired or about to expire.

      
        
        8
        +-}

      
        
        9
        +isExpired : Time.Posix -> String -> Bool

      
        
        10
        +isExpired now token =

      
        
        11
        +    let

      
        
        12
        +        expirationThreshold : number

      
        
        13
        +        expirationThreshold =

      
        
        14
        +            40 * 1000

      
        
        15
        +

      
        
        16
        +        timeDiff : Int

      
        
        17
        +        timeDiff =

      
        
        18
        +            getTokenExpiration token

      
        
        19
        +                |> (\expiration -> expiration - Time.posixToMillis now)

      
        
        20
        +    in

      
        
        21
        +    timeDiff <= expirationThreshold

      
        
        22
        +

      
        
        23
        +

      
        
        24
        +{-| Extracts the expiration time (in millis) from a JWT token.

      
        
        25
        +Returns 0 if cannot parse token.

      
        
        26
        +-}

      
        
        27
        +getTokenExpiration : String -> Int

      
        
        28
        +getTokenExpiration token =

      
        
        29
        +    Jwt.getTokenExpirationMillis token

      
        
        30
        +        |> Result.withDefault 0

      
A web/src/Pages/Home_.elm
···
        
        1
        +module Pages.Home_ exposing (Model, Msg, page)

      
        
        2
        +

      
        
        3
        +import Effect exposing (Effect)

      
        
        4
        +import Html

      
        
        5
        +import Html.Attributes as Attributes

      
        
        6
        +import Html.Events

      
        
        7
        +import Page exposing (Page)

      
        
        8
        +import Route exposing (Route)

      
        
        9
        +import Shared

      
        
        10
        +import View exposing (View)

      
        
        11
        +

      
        
        12
        +

      
        
        13
        +page : Shared.Model -> Route () -> Page Model Msg

      
        
        14
        +page shared _ =

      
        
        15
        +    Page.new

      
        
        16
        +        { init = init shared

      
        
        17
        +        , update = update

      
        
        18
        +        , subscriptions = subscriptions

      
        
        19
        +        , view = view shared

      
        
        20
        +        }

      
        
        21
        +

      
        
        22
        +

      
        
        23
        +

      
        
        24
        +-- INIT

      
        
        25
        +

      
        
        26
        +

      
        
        27
        +type alias Model =

      
        
        28
        +    {}

      
        
        29
        +

      
        
        30
        +

      
        
        31
        +init : Shared.Model -> () -> ( Model, Effect Msg )

      
        
        32
        +init _ () =

      
        
        33
        +    ( {}, Effect.none )

      
        
        34
        +

      
        
        35
        +

      
        
        36
        +

      
        
        37
        +-- UPDATE

      
        
        38
        +

      
        
        39
        +

      
        
        40
        +type Msg

      
        
        41
        +    = LogOut

      
        
        42
        +

      
        
        43
        +

      
        
        44
        +update : Msg -> Model -> ( Model, Effect Msg )

      
        
        45
        +update msg model =

      
        
        46
        +    case msg of

      
        
        47
        +        LogOut ->

      
        
        48
        +            ( model, Effect.logout )

      
        
        49
        +

      
        
        50
        +

      
        
        51
        +

      
        
        52
        +-- SUBSCRIPTIONS

      
        
        53
        +

      
        
        54
        +

      
        
        55
        +subscriptions : Model -> Sub Msg

      
        
        56
        +subscriptions _ =

      
        
        57
        +    Sub.none

      
        
        58
        +

      
        
        59
        +

      
        
        60
        +

      
        
        61
        +-- VIEW

      
        
        62
        +

      
        
        63
        +

      
        
        64
        +view : Shared.Model -> Model -> View Msg

      
        
        65
        +view _ _ =

      
        
        66
        +    { title = "Homepage"

      
        
        67
        +    , body =

      
        
        68
        +        [ Html.div []

      
        
        69
        +            [ Html.p [] [ Html.text "Hello, world!" ]

      
        
        70
        +            , Html.p []

      
        
        71
        +                [ Html.a

      
        
        72
        +                    [ Attributes.href "/profile/me" ]

      
        
        73
        +                    [ Html.text "/profile/me - fetches authorized data" ]

      
        
        74
        +                ]

      
        
        75
        +            , Html.p []

      
        
        76
        +                [ Html.button

      
        
        77
        +                    [ Html.Events.onClick LogOut ]

      
        
        78
        +                    [ Html.text "Logout" ]

      
        
        79
        +                ]

      
        
        80
        +            ]

      
        
        81
        +        ]

      
        
        82
        +    }

      
A web/src/Pages/Profile/Me.elm
···
        
        1
        +module Pages.Profile.Me exposing (Model, Msg, page)

      
        
        2
        +

      
        
        3
        +import Api

      
        
        4
        +import Api.Me

      
        
        5
        +import Auth

      
        
        6
        +import Data.Me exposing (Me)

      
        
        7
        +import Effect exposing (Effect)

      
        
        8
        +import Html exposing (Html)

      
        
        9
        +import Http

      
        
        10
        +import Page exposing (Page)

      
        
        11
        +import Route exposing (Route)

      
        
        12
        +import Shared

      
        
        13
        +import View exposing (View)

      
        
        14
        +

      
        
        15
        +

      
        
        16
        +page : Auth.User -> Shared.Model -> Route () -> Page Model Msg

      
        
        17
        +page _ shared _ =

      
        
        18
        +    Page.new

      
        
        19
        +        { init = init shared

      
        
        20
        +        , update = update

      
        
        21
        +        , subscriptions = subscriptions

      
        
        22
        +        , view = view shared

      
        
        23
        +        }

      
        
        24
        +

      
        
        25
        +

      
        
        26
        +

      
        
        27
        +-- INIT

      
        
        28
        +

      
        
        29
        +

      
        
        30
        +type alias Model =

      
        
        31
        +    { me : Api.Response Me }

      
        
        32
        +

      
        
        33
        +

      
        
        34
        +init : Shared.Model -> () -> ( Model, Effect Msg )

      
        
        35
        +init _ () =

      
        
        36
        +    ( { me = Api.Loading }

      
        
        37
        +    , Api.Me.get { onResponse = ApiMeResponded }

      
        
        38
        +    )

      
        
        39
        +

      
        
        40
        +

      
        
        41
        +

      
        
        42
        +-- UPDATE

      
        
        43
        +

      
        
        44
        +

      
        
        45
        +type Msg

      
        
        46
        +    = ApiMeResponded (Result Http.Error Me)

      
        
        47
        +

      
        
        48
        +

      
        
        49
        +update : Msg -> Model -> ( Model, Effect Msg )

      
        
        50
        +update msg model =

      
        
        51
        +    case msg of

      
        
        52
        +        ApiMeResponded (Ok userData) ->

      
        
        53
        +            ( { model | me = Api.Success userData }, Effect.none )

      
        
        54
        +

      
        
        55
        +        ApiMeResponded (Err error) ->

      
        
        56
        +            ( { model | me = Api.Failure error }, Effect.none )

      
        
        57
        +

      
        
        58
        +

      
        
        59
        +

      
        
        60
        +-- SUBSCRIPTIONS

      
        
        61
        +

      
        
        62
        +

      
        
        63
        +subscriptions : Model -> Sub Msg

      
        
        64
        +subscriptions _ =

      
        
        65
        +    Sub.none

      
        
        66
        +

      
        
        67
        +

      
        
        68
        +

      
        
        69
        +-- VIEW

      
        
        70
        +

      
        
        71
        +

      
        
        72
        +view : Shared.Model -> Model -> View Msg

      
        
        73
        +view shared model =

      
        
        74
        +    { title = "Profile"

      
        
        75
        +    , body = [ viewProfileContent shared model.me ]

      
        
        76
        +    }

      
        
        77
        +

      
        
        78
        +

      
        
        79
        +viewProfileContent : Shared.Model -> Api.Response Me -> Html Msg

      
        
        80
        +viewProfileContent shared userResponse =

      
        
        81
        +    case userResponse of

      
        
        82
        +        Api.Loading ->

      
        
        83
        +            Html.text "Loading..."

      
        
        84
        +

      
        
        85
        +        Api.Success user ->

      
        
        86
        +            viewUserDetails shared user

      
        
        87
        +

      
        
        88
        +        Api.Failure err ->

      
        
        89
        +            Html.text (Api.errorToFriendlyMessage err)

      
        
        90
        +

      
        
        91
        +

      
        
        92
        +viewUserDetails : Shared.Model -> Me -> Html Msg

      
        
        93
        +viewUserDetails _ me =

      
        
        94
        +    Html.div []

      
        
        95
        +        [ Html.p [] [ Html.text ("Email: " ++ me.email) ]

      
        
        96
        +        , Html.p [] [ Html.text ("Joined: " ++ me.createdAt) ]

      
        
        97
        +        ]

      
A web/src/Pages/SignIn.elm
···
        
        1
        +module Pages.SignIn exposing (Model, Msg, page)

      
        
        2
        +

      
        
        3
        +import Api

      
        
        4
        +import Api.Auth

      
        
        5
        +import Data.Credentials exposing (Credentials)

      
        
        6
        +import Effect exposing (Effect)

      
        
        7
        +import Html exposing (Html)

      
        
        8
        +import Html.Attributes as Attr

      
        
        9
        +import Html.Events

      
        
        10
        +import Http

      
        
        11
        +import Page exposing (Page)

      
        
        12
        +import Route exposing (Route)

      
        
        13
        +import Route.Path

      
        
        14
        +import Shared

      
        
        15
        +import View exposing (View)

      
        
        16
        +

      
        
        17
        +

      
        
        18
        +page : Shared.Model -> Route () -> Page Model Msg

      
        
        19
        +page shared _ =

      
        
        20
        +    Page.new

      
        
        21
        +        { init = init shared

      
        
        22
        +        , update = update

      
        
        23
        +        , subscriptions = subscriptions

      
        
        24
        +        , view = view

      
        
        25
        +        }

      
        
        26
        +

      
        
        27
        +

      
        
        28
        +

      
        
        29
        +-- INIT

      
        
        30
        +

      
        
        31
        +

      
        
        32
        +type alias Model =

      
        
        33
        +    { email : String

      
        
        34
        +    , password : String

      
        
        35
        +    , isSubmittingForm : Bool

      
        
        36
        +    , error : Maybe Http.Error

      
        
        37
        +    }

      
        
        38
        +

      
        
        39
        +

      
        
        40
        +init : Shared.Model -> () -> ( Model, Effect Msg )

      
        
        41
        +init shared _ =

      
        
        42
        +    ( { isSubmittingForm = False

      
        
        43
        +      , email = ""

      
        
        44
        +      , password = ""

      
        
        45
        +      , error = Nothing

      
        
        46
        +      }

      
        
        47
        +    , case shared.credentials of

      
        
        48
        +        Just _ ->

      
        
        49
        +            Effect.pushRoutePath Route.Path.Home_

      
        
        50
        +

      
        
        51
        +        Nothing ->

      
        
        52
        +            Effect.none

      
        
        53
        +    )

      
        
        54
        +

      
        
        55
        +

      
        
        56
        +

      
        
        57
        +-- UPDATE

      
        
        58
        +

      
        
        59
        +

      
        
        60
        +type Msg

      
        
        61
        +    = UserUpdatedInput Field String

      
        
        62
        +    | UserClickedSubmit

      
        
        63
        +    | ApiSignInResponded (Result Http.Error Credentials)

      
        
        64
        +

      
        
        65
        +

      
        
        66
        +type Field

      
        
        67
        +    = Email

      
        
        68
        +    | Password

      
        
        69
        +

      
        
        70
        +

      
        
        71
        +update : Msg -> Model -> ( Model, Effect Msg )

      
        
        72
        +update msg model =

      
        
        73
        +    case msg of

      
        
        74
        +        UserClickedSubmit ->

      
        
        75
        +            ( { model | isSubmittingForm = True }

      
        
        76
        +            , Api.Auth.signin

      
        
        77
        +                { onResponse = ApiSignInResponded

      
        
        78
        +                , email = model.email

      
        
        79
        +                , password = model.password

      
        
        80
        +                }

      
        
        81
        +            )

      
        
        82
        +

      
        
        83
        +        UserUpdatedInput Email email ->

      
        
        84
        +            ( { model | email = email }, Effect.none )

      
        
        85
        +

      
        
        86
        +        UserUpdatedInput Password password ->

      
        
        87
        +            ( { model | password = password }, Effect.none )

      
        
        88
        +

      
        
        89
        +        ApiSignInResponded (Ok credentials) ->

      
        
        90
        +            ( { model | isSubmittingForm = False }

      
        
        91
        +            , Effect.signin credentials

      
        
        92
        +            )

      
        
        93
        +

      
        
        94
        +        ApiSignInResponded (Err error) ->

      
        
        95
        +            ( { model | isSubmittingForm = False, error = Just error }

      
        
        96
        +            , Effect.none

      
        
        97
        +            )

      
        
        98
        +

      
        
        99
        +

      
        
        100
        +

      
        
        101
        +-- SUBSCRIPTIONS

      
        
        102
        +

      
        
        103
        +

      
        
        104
        +subscriptions : Model -> Sub Msg

      
        
        105
        +subscriptions _ =

      
        
        106
        +    Sub.none

      
        
        107
        +

      
        
        108
        +

      
        
        109
        +

      
        
        110
        +-- VIEW

      
        
        111
        +

      
        
        112
        +

      
        
        113
        +view : Model -> View Msg

      
        
        114
        +view model =

      
        
        115
        +    { title = "Sign-in"

      
        
        116
        +    , body =

      
        
        117
        +        [ Html.div []

      
        
        118
        +            [ Html.div []

      
        
        119
        +                [ Html.div []

      
        
        120
        +                    [ Html.h1 [] [ Html.text "Sign in" ]

      
        
        121
        +                    , viewError model.error

      
        
        122
        +                    , viewForm model

      
        
        123
        +                    ]

      
        
        124
        +                ]

      
        
        125
        +            ]

      
        
        126
        +        ]

      
        
        127
        +    }

      
        
        128
        +

      
        
        129
        +

      
        
        130
        +viewForm : Model -> Html Msg

      
        
        131
        +viewForm model =

      
        
        132
        +    Html.form [ Html.Events.onSubmit UserClickedSubmit ]

      
        
        133
        +        [ viewFormInput { field = Email, value = model.email }

      
        
        134
        +        , viewFormInput { field = Password, value = model.password }

      
        
        135
        +        , viewFormControls model

      
        
        136
        +        ]

      
        
        137
        +

      
        
        138
        +

      
        
        139
        +viewError : Maybe Http.Error -> Html Msg

      
        
        140
        +viewError maybeError =

      
        
        141
        +    case maybeError of

      
        
        142
        +        Just error ->

      
        
        143
        +            Html.div [ Attr.style "color" "red" ]

      
        
        144
        +                [ Html.text (Api.errorToFriendlyMessage error) ]

      
        
        145
        +

      
        
        146
        +        Nothing ->

      
        
        147
        +            Html.text ""

      
        
        148
        +

      
        
        149
        +

      
        
        150
        +viewFormInput : { field : Field, value : String } -> Html Msg

      
        
        151
        +viewFormInput opts =

      
        
        152
        +    Html.div []

      
        
        153
        +        [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ]

      
        
        154
        +        , Html.div []

      
        
        155
        +            [ Html.input

      
        
        156
        +                [ Attr.type_ (fromFieldToInputType opts.field)

      
        
        157
        +                , Attr.value opts.value

      
        
        158
        +                , Html.Events.onInput (UserUpdatedInput opts.field)

      
        
        159
        +                ]

      
        
        160
        +                []

      
        
        161
        +            ]

      
        
        162
        +        ]

      
        
        163
        +

      
        
        164
        +

      
        
        165
        +viewFormControls : Model -> Html Msg

      
        
        166
        +viewFormControls model =

      
        
        167
        +    Html.div []

      
        
        168
        +        [ Html.button

      
        
        169
        +            [ Attr.disabled model.isSubmittingForm ]

      
        
        170
        +            [ Html.text "Sign In" ]

      
        
        171
        +        ]

      
        
        172
        +

      
        
        173
        +

      
        
        174
        +fromFieldToLabel : Field -> String

      
        
        175
        +fromFieldToLabel field =

      
        
        176
        +    case field of

      
        
        177
        +        Email ->

      
        
        178
        +            "Email address"

      
        
        179
        +

      
        
        180
        +        Password ->

      
        
        181
        +            "Password"

      
        
        182
        +

      
        
        183
        +

      
        
        184
        +fromFieldToInputType : Field -> String

      
        
        185
        +fromFieldToInputType field =

      
        
        186
        +    case field of

      
        
        187
        +        Email ->

      
        
        188
        +            "email"

      
        
        189
        +

      
        
        190
        +        Password ->

      
        
        191
        +            "password"

      
A web/src/Ports.elm
···
        
        1
        +port module Ports exposing (sendToLocalStorage)

      
        
        2
        +

      
        
        3
        +import Json.Encode

      
        
        4
        +

      
        
        5
        +

      
        
        6
        +port sendToLocalStorage : { key : String, value : Json.Encode.Value } -> Cmd msg

      
A web/src/Shared.elm
···
        
        1
        +module Shared exposing

      
        
        2
        +    ( Flags, decoder

      
        
        3
        +    , Model, Msg

      
        
        4
        +    , init, update, subscriptions

      
        
        5
        +    )

      
        
        6
        +

      
        
        7
        +{-|

      
        
        8
        +

      
        
        9
        +@docs Flags, decoder

      
        
        10
        +@docs Model, Msg

      
        
        11
        +@docs init, update, subscriptions

      
        
        12
        +

      
        
        13
        +-}

      
        
        14
        +

      
        
        15
        +import Api.Auth

      
        
        16
        +import Data.Credentials exposing (Credentials)

      
        
        17
        +import Dict

      
        
        18
        +import Effect exposing (Effect)

      
        
        19
        +import Json.Decode

      
        
        20
        +import JwtUtil

      
        
        21
        +import Route exposing (Route)

      
        
        22
        +import Route.Path

      
        
        23
        +import Shared.Model

      
        
        24
        +import Shared.Msg

      
        
        25
        +import Task

      
        
        26
        +import Time

      
        
        27
        +

      
        
        28
        +

      
        
        29
        +

      
        
        30
        +-- FLAGS

      
        
        31
        +

      
        
        32
        +

      
        
        33
        +type alias Flags =

      
        
        34
        +    { accessToken : Maybe String

      
        
        35
        +    , refreshToken : Maybe String

      
        
        36
        +    }

      
        
        37
        +

      
        
        38
        +

      
        
        39
        +decoder : Json.Decode.Decoder Flags

      
        
        40
        +decoder =

      
        
        41
        +    Json.Decode.map2 Flags

      
        
        42
        +        (Json.Decode.field "access_token" (Json.Decode.maybe Json.Decode.string))

      
        
        43
        +        (Json.Decode.field "refresh_token" (Json.Decode.maybe Json.Decode.string))

      
        
        44
        +

      
        
        45
        +

      
        
        46
        +

      
        
        47
        +-- INIT

      
        
        48
        +

      
        
        49
        +

      
        
        50
        +type alias Model =

      
        
        51
        +    Shared.Model.Model

      
        
        52
        +

      
        
        53
        +

      
        
        54
        +init : Result Json.Decode.Error Flags -> Route () -> ( Model, Effect Msg )

      
        
        55
        +init flagsResult _ =

      
        
        56
        +    let

      
        
        57
        +        flags : Flags

      
        
        58
        +        flags =

      
        
        59
        +            flagsResult |> Result.withDefault { accessToken = Nothing, refreshToken = Nothing }

      
        
        60
        +

      
        
        61
        +        maybeCredentials : Maybe Credentials

      
        
        62
        +        maybeCredentials =

      
        
        63
        +            Maybe.map2

      
        
        64
        +                (\access refresh -> { accessToken = access, refreshToken = refresh })

      
        
        65
        +                flags.accessToken

      
        
        66
        +                flags.refreshToken

      
        
        67
        +

      
        
        68
        +        initModel : Model

      
        
        69
        +        initModel =

      
        
        70
        +            { credentials = maybeCredentials

      
        
        71
        +            , timeZone = Time.utc

      
        
        72
        +            , isRefreshingTokens = False

      
        
        73
        +            }

      
        
        74
        +    in

      
        
        75
        +    ( initModel

      
        
        76
        +    , Effect.batch

      
        
        77
        +        [ Time.now |> Task.perform Shared.Msg.CheckTokenExpiration |> Effect.sendCmd

      
        
        78
        +        , Time.here |> Task.perform Shared.Msg.GotZone |> Effect.sendCmd

      
        
        79
        +        ]

      
        
        80
        +    )

      
        
        81
        +

      
        
        82
        +

      
        
        83
        +

      
        
        84
        +-- UPDATE

      
        
        85
        +

      
        
        86
        +

      
        
        87
        +type alias Msg =

      
        
        88
        +    Shared.Msg.Msg

      
        
        89
        +

      
        
        90
        +

      
        
        91
        +update : Route () -> Msg -> Model -> ( Model, Effect Msg )

      
        
        92
        +update _ msg model =

      
        
        93
        +    case msg of

      
        
        94
        +        Shared.Msg.GotZone timeZone ->

      
        
        95
        +            ( { model | timeZone = timeZone }, Effect.none )

      
        
        96
        +

      
        
        97
        +        Shared.Msg.Logout ->

      
        
        98
        +            ( { model | credentials = Nothing }, Effect.clearUser )

      
        
        99
        +

      
        
        100
        +        Shared.Msg.SignedIn credentials ->

      
        
        101
        +            ( { model | credentials = Just credentials }

      
        
        102
        +            , Effect.batch

      
        
        103
        +                [ Effect.pushRoute

      
        
        104
        +                    { path = Route.Path.Home_

      
        
        105
        +                    , query = Dict.empty

      
        
        106
        +                    , hash = Nothing

      
        
        107
        +                    }

      
        
        108
        +                , Effect.saveUser credentials.accessToken credentials.refreshToken

      
        
        109
        +                ]

      
        
        110
        +            )

      
        
        111
        +

      
        
        112
        +        Shared.Msg.CheckTokenExpiration now ->

      
        
        113
        +            case model.credentials of

      
        
        114
        +                Just credentials ->

      
        
        115
        +                    if JwtUtil.isExpired now credentials.accessToken then

      
        
        116
        +                        ( model, Effect.refreshTokens )

      
        
        117
        +

      
        
        118
        +                    else

      
        
        119
        +                        ( model, Effect.none )

      
        
        120
        +

      
        
        121
        +                Nothing ->

      
        
        122
        +                    ( model, Effect.none )

      
        
        123
        +

      
        
        124
        +        Shared.Msg.TriggerTokenRefresh ->

      
        
        125
        +            case model.credentials of

      
        
        126
        +                Just credentials ->

      
        
        127
        +                    ( { model | isRefreshingTokens = True }

      
        
        128
        +                    , Api.Auth.refreshToken

      
        
        129
        +                        { onResponse = Shared.Msg.ApiRefreshTokensResponded

      
        
        130
        +                        , refreshToken = credentials.refreshToken

      
        
        131
        +                        }

      
        
        132
        +                    )

      
        
        133
        +

      
        
        134
        +                Nothing ->

      
        
        135
        +                    ( model, Effect.none )

      
        
        136
        +

      
        
        137
        +        Shared.Msg.ApiRefreshTokensResponded (Ok credentials) ->

      
        
        138
        +            ( { model | isRefreshingTokens = False, credentials = Just credentials }

      
        
        139
        +            , Effect.saveUser credentials.accessToken credentials.refreshToken

      
        
        140
        +            )

      
        
        141
        +

      
        
        142
        +        Shared.Msg.ApiRefreshTokensResponded (Err _) ->

      
        
        143
        +            ( { model | isRefreshingTokens = False }, Effect.clearUser )

      
        
        144
        +

      
        
        145
        +

      
        
        146
        +

      
        
        147
        +-- SUBSCRIPTIONS

      
        
        148
        +

      
        
        149
        +

      
        
        150
        +subscriptions : Route () -> Model -> Sub Msg

      
        
        151
        +subscriptions _ _ =

      
        
        152
        +    Time.every (30 * 1000) Shared.Msg.CheckTokenExpiration

      
A web/src/Shared/Model.elm
···
        
        1
        +module Shared.Model exposing (Model)

      
        
        2
        +

      
        
        3
        +import Data.Credentials exposing (Credentials)

      
        
        4
        +import Time

      
        
        5
        +

      
        
        6
        +

      
        
        7
        +type alias Model =

      
        
        8
        +    { credentials : Maybe Credentials

      
        
        9
        +    , timeZone : Time.Zone

      
        
        10
        +    , isRefreshingTokens : Bool

      
        
        11
        +    }

      
A web/src/Shared/Msg.elm
···
        
        1
        +module Shared.Msg exposing (Msg(..))

      
        
        2
        +

      
        
        3
        +import Data.Credentials exposing (Credentials)

      
        
        4
        +import Http

      
        
        5
        +import Time

      
        
        6
        +

      
        
        7
        +

      
        
        8
        +type Msg

      
        
        9
        +    = GotZone Time.Zone

      
        
        10
        +      -- Auth

      
        
        11
        +    | Logout

      
        
        12
        +    | SignedIn Credentials

      
        
        13
        +      -- Session

      
        
        14
        +    | CheckTokenExpiration Time.Posix

      
        
        15
        +    | TriggerTokenRefresh

      
        
        16
        +    | ApiRefreshTokensResponded (Result Http.Error Credentials)

      
A web/src/interop.js
···
        
        1
        +export const flags = (_) => {

      
        
        2
        +    return {

      
        
        3
        +        access_token: JSON.parse(window.localStorage.access_token || 'null'),

      
        
        4
        +        refresh_token: JSON.parse(window.localStorage.refresh_token || 'null'),

      
        
        5
        +    }

      
        
        6
        +}

      
        
        7
        +

      
        
        8
        +export const onReady = ({ app }) => {

      
        
        9
        +    if (app.ports?.sendToLocalStorage) {

      
        
        10
        +        app.ports.sendToLocalStorage.subscribe(({ key, value }) => {

      
        
        11
        +            window.localStorage[key] = JSON.stringify(value)

      
        
        12
        +        })

      
        
        13
        +    }

      
        
        14
        +}

      
A web/tests/UnitTests/Data/Credentiala.elm
···
        
        1
        +module UnitTests.Data.Credentiala exposing (suite)

      
        
        2
        +

      
        
        3
        +import Data.Credentials

      
        
        4
        +import Expect

      
        
        5
        +import Json.Decode as Json

      
        
        6
        +import Test exposing (Test, describe, test)

      
        
        7
        +

      
        
        8
        +

      
        
        9
        +suite : Test

      
        
        10
        +suite =

      
        
        11
        +    describe "Data.Credentials"

      
        
        12
        +        [ test "decode credentials" <|

      
        
        13
        +            \_ ->

      
        
        14
        +                """

      
        
        15
        +                {

      
        
        16
        +                    "access_token": "access.token.value",

      
        
        17
        +                    "refresh_token": "refresh-token-value"

      
        
        18
        +                }

      
        
        19
        +                """

      
        
        20
        +                    |> Json.decodeString Data.Credentials.decode

      
        
        21
        +                    |> Expect.ok

      
        
        22
        +        ]

      
A web/tests/UnitTests/Data/Me.elm
···
        
        1
        +module UnitTests.Data.Me exposing (suite)

      
        
        2
        +

      
        
        3
        +import Data.Me

      
        
        4
        +import Expect

      
        
        5
        +import Json.Decode as Json

      
        
        6
        +import Test exposing (Test, describe, test)

      
        
        7
        +

      
        
        8
        +

      
        
        9
        +suite : Test

      
        
        10
        +suite =

      
        
        11
        +    describe "Data.Me"

      
        
        12
        +        [ test "decode credentials" <|

      
        
        13
        +            \_ ->

      
        
        14
        +                """

      
        
        15
        +                {

      
        
        16
        +                  "email": "admin@onasty.local",

      
        
        17
        +                  "created_at": "2025-06-06T19:44:17.370068Z"

      
        
        18
        +                }

      
        
        19
        +                """

      
        
        20
        +                    |> Json.decodeString Data.Me.decode

      
        
        21
        +                    |> Expect.ok

      
        
        22
        +        ]