diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dce519315a47d14452434b7dd528cf06b67360e7..dfa0ce1fd6b8abf4dc10a8d895942b30e9ae4078 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - build - test + - deploy # This folder is cached between builds # https://docs.gitlab.com/ee/ci/yaml/index.html#cache @@ -21,6 +22,11 @@ build: extends: .pnpm script: - pnpm build + artifacts: + untracked: false + when: on_success + paths: + - dist/* lint: stage: test @@ -34,3 +40,13 @@ check: script: - pnpm build - pnpm check + +publish-npm: + stage: deploy + extends: .pnpm + dependencies: + - build + script: + - echo "@arise:registry=https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" >> .npmrc + - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc + - npm publish diff --git a/.npmrc b/.npmrc index b6f27f135954640c8cc5bfd7b8c9922ca6eb2aad..67f452567332ffe9ae63c1da9a4f4c216d918761 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -engine-strict=true +# https://github.com/renovatebot/renovate/issues/12068#issuecomment-939236784 +# engine-strict=true diff --git a/package.json b/package.json index 7483902c0770b3e2d3a2bdbd7e38456e10f6e53f..0001709ba694b428251f0a3b6fbcd2fde856eb76 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sveltekit", + "name": "ariseid-connect-sveltekit", "version": "0.0.1", "scripts": { "dev": "vite dev", @@ -31,6 +31,8 @@ "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.27.4", "@sveltejs/package": "^2.0.0", + "@types/cookie": "^0.6.0", + "@types/jsonwebtoken": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.28.0", @@ -48,5 +50,13 @@ }, "svelte": "./dist/index.js", "types": "./dist/index.d.ts", - "type": "module" + "type": "module", + "dependencies": { + "assert": "^2.1.0", + "cookie": "^0.6.0", + "http-status": "^1.7.3", + "jsonwebtoken": "^9.0.2", + "openid-client": "^5.6.1", + "zod": "^3.22.4" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22cd6eaeb020574cd7b0f899f9d5e71d46aa1e0f..6e48e328306128b34a930f4951e4f05cd545f391 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,26 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + assert: + specifier: ^2.1.0 + version: 2.1.0 + cookie: + specifier: ^0.6.0 + version: 0.6.0 + http-status: + specifier: ^1.7.3 + version: 1.7.3 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + openid-client: + specifier: ^5.6.1 + version: 5.6.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: '@sveltejs/adapter-auto': specifier: ^2.0.0 @@ -14,6 +34,12 @@ devDependencies: '@sveltejs/package': specifier: ^2.0.0 version: 2.2.3(svelte@4.2.8)(typescript@5.3.3) + '@types/cookie': + specifier: ^0.6.0 + version: 0.6.0 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.5 '@typescript-eslint/eslint-plugin': specifier: ^6.0.0 version: 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.3) @@ -502,6 +528,10 @@ packages: resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true @@ -510,6 +540,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.10.4 + dev: true + /@types/node@20.10.4: resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} dependencies: @@ -765,10 +801,25 @@ packages: engines: {node: '>=8'} dev: true + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.5 + is-nan: 1.3.2 + object-is: 1.1.5 + object.assign: 4.1.5 + util: 0.12.5 + dev: false + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -808,11 +859,23 @@ packages: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} dev: true + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -890,6 +953,11 @@ packages: engines: {node: '>= 0.6'} dev: true + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -945,6 +1013,24 @@ packages: engines: {node: '>=0.10.0'} dev: true + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -978,6 +1064,12 @@ packages: esutils: 2.0.3 dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: true @@ -1231,6 +1323,12 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -1243,10 +1341,23 @@ packages: dev: true optional: true + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1310,6 +1421,12 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -1323,6 +1440,41 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-status@1.7.3: + resolution: {integrity: sha512-GS8tL1qHT2nBCMJDYMHGkkkKQLNkIAHz37vgO68XKvzv+XyqB4oh/DfmMHdtRzfqSJPj1xKG2TaELZtlCz6BEQ==} + engines: {node: '>= 0.4.0'} + dev: false + /ignore-walk@5.0.1: resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -1361,7 +1513,14 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -1370,11 +1529,23 @@ packages: binary-extensions: 2.2.0 dev: true + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1382,6 +1553,14 @@ packages: is-extglob: 2.1.1 dev: true + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1398,10 +1577,21 @@ packages: '@types/estree': 1.0.5 dev: true + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jose@4.15.4: + resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} + dev: false + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1425,6 +1615,37 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.2 + semver: 7.5.4 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -1469,10 +1690,38 @@ packages: p-locate: 5.0.0 dev: true + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -1490,7 +1739,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} @@ -1573,7 +1821,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -1620,12 +1867,54 @@ packages: npm-normalize-package-bin: 2.0.0 dev: true + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /oidc-token-hash@5.0.3: + resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} + engines: {node: ^10.13.0 || >=12.0.0} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: true + /openid-client@5.6.1: + resolution: {integrity: sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==} + dependencies: + jose: 4.15.4 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.0.3 + dev: false + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -1883,6 +2172,10 @@ packages: mri: 1.2.0 dev: true + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + /sander@0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} dependencies: @@ -1898,12 +2191,21 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: true + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2219,6 +2521,16 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.13 + dev: false + /vite-node@0.34.6(@types/node@20.10.4): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} @@ -2353,6 +2665,17 @@ packages: - terser dev: true + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2376,7 +2699,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} @@ -2392,3 +2714,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false diff --git a/src/app.d.ts b/src/app.d.ts index 899c7e8fcaa1cc213388dc711f798ae20b622420..0ae5781bd9087e15099b690ddf6d733c8227e6ad 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,9 +1,14 @@ // See https://kit.svelte.dev/docs/types#app + +import type { AriseData } from "$lib/index.ts"; + // for information about these interfaces declare global { namespace App { // interface Error {} - // interface Locals {} + interface Locals { + arise: AriseData; + } // interface PageData {} // interface Platform {} } diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts new file mode 100644 index 0000000000000000000000000000000000000000..feb8630e1d6369847e7e28b909cc753b883e6463 --- /dev/null +++ b/src/lib/cookies.ts @@ -0,0 +1,50 @@ +import { JWTCookieManager } from "$lib/utils/jwt_cookie.js"; +import type { Cookies } from "@sveltejs/kit"; +import type { TokenSet } from "openid-client"; +import { z } from "zod"; +import type { InternalConfig } from "$lib/types.js"; + +const ONE_HOUR = 60 * 60; +const ONE_DAY = 24 * ONE_HOUR; +const ONE_MONTH = 30 * ONE_DAY; + +export const authSchema = z.object({ + codeVerifier: z.string(), + state: z.string(), + nonce: z.string(), +}); + +export const authCookie = new JWTCookieManager(authSchema, { + serialize: { path: "/", maxAge: ONE_HOUR }, + name: "tmp_arise_auth_secrets", +}); + +export const tokenSetSchema = z.object({ + access_token: z.string().optional(), + token_type: z.string().optional(), + id_token: z.string().optional(), + refresh_token: z.string().optional(), + scope: z.string().optional(), + expires_at: z.number().optional(), + session_state: z.string().optional(), +}); + +export const tokenSetCookie = new JWTCookieManager(tokenSetSchema, { + name: "arise_token_set", + serialize: { + maxAge: ONE_MONTH, + }, +}); + +export async function setTokenSetCookie( + tokenSet: TokenSet, + cookies: Cookies, + { client }: InternalConfig, +) { + const userinfo = await client.userinfo(tokenSet); + + tokenSetCookie.send(cookies, { + payload: tokenSet, + jwt: { subject: userinfo.sub, expiresIn: ONE_MONTH }, + }); +} diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000000000000000000000000000000000000..1eae2309f7ff8f9a4f4aa0541ad0381267f257c5 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,13 @@ +import { ensureEnv } from "$lib/utils/env.js"; +import { ensurePublicEnv } from "$lib/utils/public_env.js"; + +export const env = ensureEnv([ + "API_URL", + "API_TOKEN", + "JWT_SECRET", + "AIDC_CLIENT_ID", + "AIDC_CLIENT_SECRET", + "ORIGIN", +] as const); + +export const publicEnv = ensurePublicEnv(["PUBLIC_MAPTILER_KEY"] as const); diff --git a/src/lib/handlers/cookie.ts b/src/lib/handlers/cookie.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ec905248c228a0d09d05789deb07d369856ab07 --- /dev/null +++ b/src/lib/handlers/cookie.ts @@ -0,0 +1,32 @@ +import { TokenSet } from "openid-client"; +import { setTokenSetCookie, tokenSetCookie } from "$lib/cookies.js"; +import type { Handle } from "@sveltejs/kit"; +import type { InternalConfig } from "$lib/types.js"; + +export default function (config: InternalConfig): Handle { + return async function ({ event, resolve }) { + const jwtTokenSet = tokenSetCookie.receive(event.cookies); + + if (jwtTokenSet !== null) { + let tokenSet = new TokenSet(jwtTokenSet); + + try { + if (tokenSet.expired()) { + tokenSet = await config.client.refresh(tokenSet); + + await setTokenSetCookie(tokenSet, event.cookies, config); + } + + event.locals.arise.loggedIn = { + user: { id: tokenSet.claims().sub }, + tokenSet, + }; + } catch (error) { + console.error(error); + tokenSetCookie.delete(event.cookies); + } + } + + return resolve(event); + }; +} diff --git a/src/lib/handlers/index.ts b/src/lib/handlers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..031158d9dc681830f10279cf3bf72b2850e5fadd --- /dev/null +++ b/src/lib/handlers/index.ts @@ -0,0 +1,35 @@ +import { sequence } from "@sveltejs/kit/hooks"; +import cookieHandler from "./cookie"; +import loginHandler from "./login"; +import logoutHandler from "./logout"; +import loginCallbackHandler from "./loginCallback"; +import logoutCallbackHandler from "./logoutCallback"; +import type { Handle } from "@sveltejs/kit"; +import type { InternalConfig } from "../types"; + +const init: Handle = ({ event, resolve }) => { + event.locals.arise = {}; + return resolve(event); +}; + +function route(pathname: string, handler: Handle): Handle { + return ({ event, resolve }) => { + if (event.url.pathname === pathname) { + return handler({ event, resolve }); + } + + return resolve(event); + }; +} + +export default function (config: InternalConfig): Handle { + const { paths } = config; + return sequence( + init, + cookieHandler(config), + route(paths.login, loginHandler(config)), + route(paths.logout, logoutHandler(config)), + route(paths.callback, loginCallbackHandler(config)), + route(paths.logoutCallback, logoutCallbackHandler(config)), + ); +} diff --git a/src/lib/handlers/login.ts b/src/lib/handlers/login.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1dca7796282bdfdcd7451a4d92ccf10a7a6467d --- /dev/null +++ b/src/lib/handlers/login.ts @@ -0,0 +1,31 @@ +import { redirect } from "@sveltejs/kit"; +import { generators } from "openid-client"; +import { authCookie } from "../cookies"; +import type { Handle } from "@sveltejs/kit"; +import type { InternalConfig } from "../types"; +import { SEE_OTHER } from "http-status"; + +export default function (config: InternalConfig): Handle { + return async function ({ event }) { + const codeVerifier = generators.codeVerifier(); + const state = generators.state(); + const nonce = generators.nonce(); + + authCookie.send(event.cookies, { + payload: { codeVerifier, state, nonce }, + }); + + const redirectURI = new URL(config.paths.callback, event.url).toString(); + const authorizationUrl = config.client.authorizationUrl({ + response_type: "code", + scope: config.scope, + code_challenge: generators.codeChallenge(codeVerifier), + code_challenge_method: "S256", + redirect_uri: redirectURI, + state, + nonce, + }); + + throw redirect(SEE_OTHER, authorizationUrl); + }; +} diff --git a/src/lib/handlers/loginCallback.ts b/src/lib/handlers/loginCallback.ts new file mode 100644 index 0000000000000000000000000000000000000000..1de08b75aa057e38cfcd3707d10fdd2ad37d34bd --- /dev/null +++ b/src/lib/handlers/loginCallback.ts @@ -0,0 +1,53 @@ +import { type Handle, redirect } from "@sveltejs/kit"; +import { authCookie, setTokenSetCookie } from "../cookies"; +import { errors } from "openid-client"; +import type { InternalConfig } from "../types"; +import { base } from "$app/paths"; +import { SEE_OTHER } from "http-status"; + +export default function (config: InternalConfig): Handle { + return async function ({ event, resolve }) { + const params = config.client.callbackParams(event.url.toString()); + + const cookie = authCookie.receive(event.cookies); + + if (cookie === null) { + throw redirect(SEE_OTHER, config.paths.login); + } + + try { + const redirectURI = new URL(config.paths.callback, event.url).toString(); + const tokenSet = await config.client.callback(redirectURI, params, { + code_verifier: cookie.codeVerifier, + state: cookie.state, + nonce: cookie.nonce, + }); + + await setTokenSetCookie(tokenSet, event.cookies, config); + } catch (error) { + if (error instanceof errors.OPError) { + if (error.error === "access_denied") { + if (config.on?.accessDenied) { + return config.on.accessDenied(event, error); + } + + throw redirect(SEE_OTHER, `${base}/`); + } + + event.locals.arise.error = error; + return resolve(event); + } else if (error instanceof errors.RPError) { + event.locals.arise.error = error; + return resolve(event); + } + + throw error; + } + + if (config.on?.loggedIn) { + return config.on.loggedIn(event); + } + + throw redirect(SEE_OTHER, `${base}/`); + }; +} diff --git a/src/lib/handlers/logout.ts b/src/lib/handlers/logout.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0f280d79f7d21c522327983920b059c94e97dda --- /dev/null +++ b/src/lib/handlers/logout.ts @@ -0,0 +1,19 @@ +import { redirect } from "@sveltejs/kit"; +import type { Handle } from "@sveltejs/kit"; +import type { InternalConfig } from "../types"; +import { SEE_OTHER } from "http-status"; + +export default function (config: InternalConfig): Handle { + return async function ({ event }) { + const postLogoutRedirectUTI = new URL( + config.paths.logoutCallback, + event.url, + ).toString(); + const endSessionUrl = config.client.endSessionUrl({ + post_logout_redirect_uri: postLogoutRedirectUTI, + id_token_hint: event.locals.arise.loggedIn?.tokenSet, + }); + + throw redirect(SEE_OTHER, endSessionUrl); + }; +} diff --git a/src/lib/handlers/logoutCallback.ts b/src/lib/handlers/logoutCallback.ts new file mode 100644 index 0000000000000000000000000000000000000000..0452d650a18a1de6ec73594e5a4dd47818b57f85 --- /dev/null +++ b/src/lib/handlers/logoutCallback.ts @@ -0,0 +1,16 @@ +import { type Handle, redirect } from "@sveltejs/kit"; +import { tokenSetCookie } from "../cookies"; +import type { InternalConfig } from "../types"; +import { SEE_OTHER } from "http-status"; + +export default function (config: InternalConfig): Handle { + return async function ({ event }) { + tokenSetCookie.delete(event.cookies); + + if (config.on?.loggedOut) { + return config.on.loggedOut(event); + } + + throw redirect(SEE_OTHER, "/"); + }; +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 47d3c46fb8964ff26d98993ccffdc2cecf44aecc..b70ab83a3ad6176805e409e5a56b23aa601e3bf5 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,38 @@ -// Reexport your entry components here +import { Issuer } from "openid-client"; +import { base } from "$app/paths"; +import handler from "./handlers/index.js"; +import type { Config } from "./types.js"; +import * as paths from "./paths.js"; + +export type { AuthData as AriseData, Config, Locals } from "./types.js"; +export { handleBuilder as ariseIdConnectBuilder } from "./index.js"; +export * as authPaths from "./paths.js"; + +function addBase(path: string): string { + return `${base}${path}`; +} + +export async function handleBuilder(config: Config) { + const issuer = await Issuer.discover( + config.issuer || "https://oidc.iiens.net/.well-known/openid-configuration", + ); + + const client = new issuer.Client({ + ...config, + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code", "id_token"], + }); + + return handler({ + ...config, + paths: { + callback: addBase(config.paths?.callback ?? paths.CALLBACK), + logoutCallback: addBase( + config.paths?.logoutCallback ?? paths.LOGOUT_CALLBACK, + ), + login: addBase(config.paths?.login ?? paths.LOGIN), + logout: addBase(config.paths?.logout ?? paths.LOGOUT), + }, + client, + }); +} diff --git a/src/lib/paths.ts b/src/lib/paths.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b28a509094127b33cfd0e4bd7179ef30731918a --- /dev/null +++ b/src/lib/paths.ts @@ -0,0 +1,5 @@ +export const BASE = "/auth"; +export const CALLBACK = `${BASE}/callback`; +export const LOGOUT_CALLBACK = `${BASE}/callback/logout`; +export const LOGIN = `${BASE}/login`; +export const LOGOUT = `${BASE}/logout`; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..52379254e98ded50db03057c55532a072f8ca70e --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,48 @@ +import type { MaybePromise, RequestEvent } from "@sveltejs/kit"; +import type { + BaseClient, + ClientMetadata, + errors, + TokenSet, +} from "openid-client"; + +export type AuthData = { + loggedIn?: { + user: { + id: string; + }; + tokenSet: TokenSet; + }; + error?: errors.OPError | errors.RPError; +}; + +export interface Locals { + arise: AuthData; +} + +type Paths = { + callback: string; + logoutCallback: string; + login: string; + logout: string; +}; + +export interface Config extends ClientMetadata { + client_secret: string; + scope: string; + paths?: Partial<Paths>; + issuer?: string; + on?: { + accessDenied?: ( + event: RequestEvent, + error: errors.OPError, + ) => MaybePromise<Response>; + loggedIn?: (event: RequestEvent) => MaybePromise<Response>; + loggedOut?: (event: RequestEvent) => MaybePromise<Response>; + }; +} + +export interface InternalConfig extends Config { + client: BaseClient; + paths: Paths; +} diff --git a/src/lib/utils/env.ts b/src/lib/utils/env.ts new file mode 100644 index 0000000000000000000000000000000000000000..50f5fb67037411f6077976a5a38e8169e9129d2e --- /dev/null +++ b/src/lib/utils/env.ts @@ -0,0 +1,18 @@ +import * as assert from "assert"; +import { env } from "$env/dynamic/private"; + +type RecordFromKeys<T extends readonly string[]> = Record<T[number], string> & + Partial<typeof env>; + +export function ensureEnv<K extends readonly string[]>( + keys: K, +): RecordFromKeys<K> { + for (const key of keys) { + const value = env[key]; + if (!env.CI) { + assert.ok(value, `Variable d'environnement ${key} manquante`); + } + } + + return env as RecordFromKeys<K>; +} diff --git a/src/lib/utils/jwt_cookie.ts b/src/lib/utils/jwt_cookie.ts new file mode 100644 index 0000000000000000000000000000000000000000..04b98d8708f0a7ef0eade41b6e121d0b4454bd97 --- /dev/null +++ b/src/lib/utils/jwt_cookie.ts @@ -0,0 +1,75 @@ +import type { Cookies } from "@sveltejs/kit"; +import jwt from "jsonwebtoken"; +import { env } from "$lib/env.js"; +import type { z, ZodObject, ZodSchema } from "zod"; +import type { CookieSerializeOptions } from "cookie"; +import { dev } from "$app/environment"; + +type Empty = Record<string, never>; + +export type JWT<T extends ZodSchema = ZodObject<Empty>> = jwt.JwtPayload & + z.output<T>; + +export interface FlashSendOptions<T> { + payload: T; + jwt?: jwt.SignOptions; +} + +export interface CookieOptions { + serialize?: CookieSerializeOptions; + name?: string; +} + +export class JWTCookieManager<T extends ZodSchema = ZodObject<Empty>> { + constructor( + private schema: T, + private cookieOptions?: CookieOptions, + ) {} + + protected cookieName(): string { + return this.cookieOptions?.name ?? "flash"; + } + protected cookieSerializeOptions(): CookieSerializeOptions { + return { + path: "/", + httpOnly: true, + secure: !dev, + sameSite: !dev, + ...this.cookieOptions?.serialize, + }; + } + + send(cookies: Cookies, options: FlashSendOptions<z.output<T>>) { + const payload = this.schema.safeParse(options.payload); + + if (!payload.success) { + throw payload.error; + } + + const cookie = jwt.sign(payload.data, env.JWT_SECRET, options.jwt); + cookies.set(this.cookieName(), cookie, this.cookieSerializeOptions()); + } + + receive(cookies: Cookies): JWT<T> | null { + const token = cookies.get(this.cookieName()); + + if (!token) return null; + + try { + jwt.verify(token, env.JWT_SECRET); + } catch { + return null; + } + + const payload = jwt.decode(token, { json: true }); + if (payload) { + const { success } = this.schema.safeParse(payload); + return success ? (payload as JWT<T>) : null; + } + return null; + } + + delete(cookies: Cookies) { + cookies.delete(this.cookieName(), this.cookieSerializeOptions()); + } +} diff --git a/src/lib/utils/public_env.ts b/src/lib/utils/public_env.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7085c4f8d9d2a09110b6a39421963f20459ff8b --- /dev/null +++ b/src/lib/utils/public_env.ts @@ -0,0 +1,21 @@ +import * as assert from "assert"; +import { env } from "$env/dynamic/public"; +import { browser } from "$app/environment"; + +type RecordFromKeys<T extends readonly string[]> = Record<T[number], string> & + Partial<typeof env>; + +export function ensurePublicEnv<K extends readonly `PUBLIC_${string}`[]>( + keys: K, +): RecordFromKeys<K> { + for (const key of keys) { + const value = env[key]; + if (!browser) { + assert.ok(value, `Variable d'environnement ${key} manquante`); + } else { + console.error("Variable d'environnement manquante"); + } + } + + return env as RecordFromKeys<K>; +}