From 190ed7f3ad4bcc207bc390265001a61cc16e626c Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Tue, 11 Mar 2025 16:43:28 -0500 Subject: [PATCH] add support for latest vite version - remove `json` usage for `data` - update tests - update types --- package-lock.json | 159 ++++++++++++++++------------- package.json | 5 +- src/authkit-callback-route.spec.ts | 15 +-- src/authkit-callback-route.ts | 4 +- src/interfaces.ts | 4 +- src/session.spec.ts | 61 +++++++---- src/session.ts | 139 ++++++++++++++++++++++--- src/test-utils/test-helpers.ts | 2 + src/utils.ts | 36 +++++++ 9 files changed, 302 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94c1371..2c9e276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,12 @@ "version": "0.8.0", "license": "MIT", "dependencies": { - "@workos-inc/node": "^7.31.0", + "@workos-inc/node": "^7.41.0", "iron-session": "^8.0.1", "jose": "^5.2.3" }, "devDependencies": { + "@remix-run/node": "^2.16.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", @@ -31,7 +32,7 @@ "typescript-eslint": "^7.2.0" }, "peerDependencies": { - "@remix-run/node": "^2.4.1", + "@remix-run/node": ">=2.4.1", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } @@ -1247,18 +1248,19 @@ } }, "node_modules/@remix-run/node": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.9.2.tgz", - "integrity": "sha512-2Mt2107pfelz4T+ziDBef3P4A7kgPqCDshnEYCVGxInivJ3HHwAKUcb7MhGa8uMMMA6LMWxbAPYNHPzC3iKv2A==", - "peer": true, + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.16.0.tgz", + "integrity": "sha512-9yYBYCHYO1+bIScGAtOy5/r4BoTS8E5lpQmjWP99UxSCSiKHPEO76V9Z8mmmarTNis/FPN+sUwfmbQWNHLA2vw==", + "dev": true, + "license": "MIT", "dependencies": { - "@remix-run/server-runtime": "2.9.2", + "@remix-run/server-runtime": "2.16.0", "@remix-run/web-fetch": "^4.4.2", "@web3-storage/multipart-parser": "^1.0.0", "cookie-signature": "^1.1.0", "source-map-support": "^0.5.21", "stream-slice": "^0.1.2", - "undici": "^6.10.1" + "undici": "^6.11.1" }, "engines": { "node": ">=18.0.0" @@ -1273,27 +1275,29 @@ } }, "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", - "peer": true, + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/@remix-run/server-runtime": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.9.2.tgz", - "integrity": "sha512-dX37FEeMVVg7KUbpRhX4hD0nUY0Sscz/qAjU4lYCdd6IzwJGariTmz+bQTXKCjploZuXj09OQZHSOS/ydkUVDA==", - "peer": true, + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.16.0.tgz", + "integrity": "sha512-gbuc4slxPi+pT47MrUYprX/wCuDlYL6H3LHZSvimWO1kDCBt8oefHzdHDPjLi4B1xzqXZomswTbuJzpZ7xRRTg==", + "dev": true, + "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.1", + "@remix-run/router": "1.23.0", "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.6.0", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3", - "turbo-stream": "^2.0.0" + "turbo-stream": "2.4.0" }, "engines": { "node": ">=18.0.0" @@ -1311,7 +1315,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz", "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==", - "peer": true, + "dev": true, "dependencies": { "@remix-run/web-stream": "^1.1.0", "web-encoding": "1.1.5" @@ -1321,7 +1325,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz", "integrity": "sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==", - "peer": true, + "dev": true, "dependencies": { "@remix-run/web-blob": "^3.1.0", "@remix-run/web-file": "^3.1.0", @@ -1340,7 +1344,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@remix-run/web-file/-/web-file-3.1.0.tgz", "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==", - "peer": true, + "dev": true, "dependencies": { "@remix-run/web-blob": "^3.1.0" } @@ -1349,7 +1353,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz", "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==", - "peer": true, + "dev": true, "dependencies": { "web-encoding": "1.1.5" } @@ -1358,7 +1362,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@remix-run/web-stream/-/web-stream-1.1.0.tgz", "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==", - "peer": true, + "dev": true, "dependencies": { "web-streams-polyfill": "^3.1.1" } @@ -1588,7 +1592,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@types/cookies": { "version": "0.9.0", @@ -2044,12 +2049,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==", - "peer": true + "dev": true }, "node_modules/@workos-inc/node": { - "version": "7.31.0", - "resolved": "https://registry.npmjs.org/@workos-inc/node/-/node-7.31.0.tgz", - "integrity": "sha512-HYiIi0f20wTvPka0Xwh6TpPE1xeRvpPcEQL8G5nDpykhUujce/D8ECrF85gJjscGgPjnmgqW8K520DshwqnHNg==", + "version": "7.41.0", + "resolved": "https://registry.npmjs.org/@workos-inc/node/-/node-7.41.0.tgz", + "integrity": "sha512-CFsKL+I/xI9zVYRuzT77z+4J7BXIXH4eX96lx7vaVZitIHIs2sfb8ePwL9PipZOGaGOaRVLxfrSfgQ7M1BNgCA==", + "license": "MIT", "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", @@ -2125,8 +2131,8 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "optional": true, - "peer": true + "dev": true, + "optional": true }, "node_modules/abab": { "version": "2.0.6", @@ -2139,7 +2145,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "peer": true, + "dev": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -2341,7 +2347,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "peer": true, + "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2596,13 +2602,14 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "peer": true, + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2785,7 +2792,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", - "peer": true, + "dev": true, "engines": { "node": ">=6.6.0" } @@ -2880,7 +2887,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "peer": true, + "dev": true, "engines": { "node": ">= 6" } @@ -2955,7 +2962,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "peer": true, + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3121,7 +3128,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "peer": true, + "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -3133,7 +3140,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "peer": true, + "dev": true, "engines": { "node": ">= 0.4" } @@ -3422,7 +3429,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "peer": true, + "dev": true, "engines": { "node": ">=6" } @@ -3612,7 +3619,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "peer": true, + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -3655,6 +3662,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3681,7 +3689,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "peer": true, + "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -3805,7 +3813,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "peer": true, + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3838,7 +3846,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "peer": true, + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -3850,7 +3858,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "peer": true, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3862,7 +3870,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "peer": true, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3874,7 +3882,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "peer": true, + "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -3889,6 +3897,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4057,7 +4066,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/iron-session": { "version": "8.0.1", @@ -4085,7 +4095,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "peer": true, + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4107,7 +4117,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "peer": true, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4161,7 +4171,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "peer": true, + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4224,7 +4234,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "peer": true, + "dev": true, "dependencies": { "which-typed-array": "^1.1.14" }, @@ -5578,7 +5588,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "peer": true, + "dev": true, "engines": { "node": ">=10" } @@ -5901,7 +5911,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "peer": true, + "dev": true, "engines": { "node": ">= 0.4" } @@ -6257,16 +6267,17 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "peer": true + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "peer": true, + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -6325,7 +6336,8 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "peer": true, + "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } @@ -6334,7 +6346,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "peer": true, + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6344,7 +6356,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "peer": true, + "dev": true, "engines": { "node": ">=0.10.0" } @@ -6380,7 +6392,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==", - "peer": true + "dev": true }, "node_modules/string-length": { "version": "4.0.2", @@ -6689,10 +6701,11 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/turbo-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.2.0.tgz", - "integrity": "sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==", - "peer": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "dev": true, + "license": "ISC" }, "node_modules/type-check": { "version": "0.4.0", @@ -6731,7 +6744,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6960,7 +6973,7 @@ "version": "6.18.2", "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.2.tgz", "integrity": "sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==", - "peer": true, + "dev": true, "engines": { "node": ">=18.17" } @@ -7032,7 +7045,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "peer": true, + "dev": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -7086,7 +7099,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "peer": true, + "dev": true, "dependencies": { "util": "^0.12.3" }, @@ -7098,7 +7111,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "peer": true, + "dev": true, "engines": { "node": ">= 8" } @@ -7162,7 +7175,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "peer": true, + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", diff --git a/package.json b/package.json index 934cac8..5c210e9 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,17 @@ "format": "prettier --write \"{src,__tests__}/**/*.{js,ts,tsx}\"" }, "dependencies": { - "@workos-inc/node": "^7.31.0", + "@workos-inc/node": "^7.41.0", "iron-session": "^8.0.1", "jose": "^5.2.3" }, "peerDependencies": { - "@remix-run/node": "^2.4.1", + "@remix-run/node": "^2.16.0", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" }, "devDependencies": { + "@remix-run/node": "^2.16.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", diff --git a/src/authkit-callback-route.spec.ts b/src/authkit-callback-route.spec.ts index 626fc41..1eac002 100644 --- a/src/authkit-callback-route.spec.ts +++ b/src/authkit-callback-route.spec.ts @@ -1,4 +1,3 @@ -import type { LoaderFunction } from '@remix-run/node'; import { getWorkOS } from './workos.js'; import { authLoader } from './authkit-callback-route.js'; import { @@ -7,6 +6,8 @@ import { assertIsResponse, } from './test-utils/test-helpers.js'; import { configureSessionStorage } from './sessionStorage.js'; +import { isDataWithResponseInit } from './utils.js'; +import { DataWithResponseInit } from './interfaces.js'; // Mock dependencies const fakeWorkosInstance = { @@ -21,7 +22,7 @@ jest.mock('./workos.js', () => ({ })); describe('authLoader', () => { - let loader: LoaderFunction; + let loader: ReturnType; let request: Request; const workos = getWorkOS(); const authenticateWithCode = jest.mocked(workos.userManagement.authenticateWithCode); @@ -58,17 +59,19 @@ describe('authLoader', () => { it('should handle authentication failure', async () => { authenticateWithCode.mockRejectedValue(new Error('Auth failed')); request = createRequestWithSearchParams(request, { code: 'invalid-code' }); - const response = (await loader({ request, params: {}, context: {} })) as Response; + const response = (await loader({ request, params: {}, context: {} })) as DataWithResponseInit; + expect(isDataWithResponseInit(response)).toBeTruthy(); - expect(response.status).toBe(500); + expect(response?.init?.status).toBe(500); }); it('should handle authentication failure with string error', async () => { authenticateWithCode.mockRejectedValue('Auth failed'); request = createRequestWithSearchParams(request, { code: 'invalid-code' }); - const response = (await loader({ request, params: {}, context: {} })) as Response; + const response = (await loader({ request, params: {}, context: {} })) as DataWithResponseInit; + expect(isDataWithResponseInit(response)).toBeTruthy(); - expect(response.status).toBe(500); + expect(response?.init?.status).toBe(500); }); }); diff --git a/src/authkit-callback-route.ts b/src/authkit-callback-route.ts index 767f13b..31be370 100644 --- a/src/authkit-callback-route.ts +++ b/src/authkit-callback-route.ts @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, json, redirect } from '@remix-run/node'; +import { LoaderFunctionArgs, data, redirect } from '@remix-run/node'; import { getConfig } from './config.js'; import { HandleAuthOptions } from './interfaces.js'; import { encryptSession } from './session.js'; @@ -86,7 +86,7 @@ export function authLoader(options: HandleAuthOptions = {}) { } function errorResponse() { - return json( + return data( { error: { message: 'Something went wrong', diff --git a/src/interfaces.ts b/src/interfaces.ts index b5baacc..296cce2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,6 +1,8 @@ -import type { SessionStorage, SessionIdStorageStrategy } from '@remix-run/node'; +import type { SessionStorage, SessionIdStorageStrategy, data } from '@remix-run/node'; import type { OauthTokens, User } from '@workos-inc/node'; +export type DataWithResponseInit = ReturnType>; + export interface HandleAuthOptions { returnPathname?: string; onSuccess?: (data: AuthLoaderSuccessData) => void | Promise; diff --git a/src/session.spec.ts b/src/session.spec.ts index 2f15500..84cac69 100644 --- a/src/session.spec.ts +++ b/src/session.spec.ts @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, Session as RemixSession, redirect } from '@remix-run/node'; +import { LoaderFunctionArgs, Session as ReactRouterSession, redirect } from '@remix-run/node'; import { AuthenticationResponse } from '@workos-inc/node'; import * as ironSession from 'iron-session'; import * as jose from 'jose'; @@ -40,6 +40,23 @@ const getSessionStorage = jest.mocked(getSessionStorageMock); const configureSessionStorage = jest.mocked(configureSessionStorageMock); const jwtVerify = jest.mocked(jose.jwtVerify); +function getHeaderValue(headers: HeadersInit | undefined, name: string): string | null { + if (!headers) { + return null; + } + + if (headers instanceof Headers) { + return headers.get(name); + } + + if (Array.isArray(headers)) { + const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase()); + return pair?.[1] ?? null; + } + + return headers[name] ?? null; +} + jest.mock('jose', () => ({ createRemoteJWKSet: jest.fn(), jwtVerify: jest.fn(), @@ -55,7 +72,7 @@ jest.mock('iron-session', () => ({ describe('session', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const createMockSession = (overrides?: Record): RemixSession => + const createMockSession = (overrides?: Record): ReactRouterSession => ({ has: jest.fn(), get: jest.fn(), @@ -65,7 +82,7 @@ describe('session', () => { id: 'test-session-id', data: {}, ...overrides, - }) satisfies RemixSession; + }) satisfies ReactRouterSession; const createMockRequest = (cookie = 'test-cookie', url = 'http://example.com./some-path') => new Request(url, { @@ -111,8 +128,10 @@ describe('session', () => { profilePictureUrl: 'https://example.com/avatar.jpg', firstName: 'Test', lastName: 'User', + externalId: null, createdAt: '2021-01-01T00:00:00Z', updatedAt: '2021-01-01T00:00:00Z', + lastSignInAt: '2021-01-01T00:00:00Z', }, impersonator: undefined, headers: {}, @@ -190,7 +209,7 @@ describe('session', () => { // Execute const response = await terminateSession(createMockRequest()); - // Assert response is instance of Remix Response + // Assert response is instance of Response expect(response instanceof Response).toBe(true); expect(response.status).toBe(302); expect(response.headers.get('Location')).toBe('https://auth.workos.com/logout/test-session-id'); @@ -222,8 +241,7 @@ describe('session', () => { }); it('should return unauthorized data when no session exists', async () => { - const response = await authkitLoader(createLoaderArgs(createMockRequest())); - const data = await response.json(); + const { data } = await authkitLoader(createLoaderArgs(createMockRequest())); expect(data).toEqual({ user: null, @@ -256,11 +274,14 @@ describe('session', () => { }); const customLoader = jest.fn().mockReturnValue(redirectResponse); - const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); - - expect(response.status).toBe(302); - expect(response.headers.get('Location')).toBe('/dashboard'); - expect(response.headers.get('X-Redirect-Reason')).toBe('test'); + try { + await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); + } catch (response: unknown) { + assertIsResponse(response); + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toEqual('/dashboard'); + expect(response.headers.get('X-Redirect-Reason')).toEqual('test'); + } }); }); @@ -303,8 +324,7 @@ describe('session', () => { }); it('should return authorized data with session claims', async () => { - const response = await authkitLoader(createLoaderArgs(createMockRequest())); - const data = await response.json(); + const { data } = await authkitLoader(createLoaderArgs(createMockRequest())); expect(data).toEqual({ user: mockSessionData.user, @@ -325,8 +345,7 @@ describe('session', () => { metadata: { key: 'value' }, }); - const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); - const data = await response.json(); + const { data } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); expect(data).toEqual( expect.objectContaining({ @@ -349,12 +368,11 @@ describe('session', () => { }), ); - const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); + const { data, init } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader); - expect(response.headers.get('Custom-Header')).toBe('test-header'); - expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8'); + expect(getHeaderValue(init?.headers, 'Custom-Header')).toBe('test-header'); + expect(getHeaderValue(init?.headers, 'Content-Type')).toBe('application/json; charset=utf-8'); - const data = await response.json(); expect(data).toEqual( expect.objectContaining({ customData: 'test-value', @@ -437,8 +455,7 @@ describe('session', () => { }); it('should refresh session when access token is invalid', async () => { - const response = await authkitLoader(createLoaderArgs(createMockRequest())); - const data = await response.json(); + const { data, init } = await authkitLoader(createLoaderArgs(createMockRequest())); // Verify the refresh token flow was triggered expect(authenticateWithRefreshToken).toHaveBeenCalledWith({ @@ -459,7 +476,7 @@ describe('session', () => { ); // Verify cookie was set - expect(response.headers.get('Set-Cookie')).toBe('new-session-cookie'); + expect(getHeaderValue(init?.headers, 'Set-Cookie')).toBe('new-session-cookie'); }); it('should redirect to root when refresh fails', async () => { diff --git a/src/session.ts b/src/session.ts index 0b66d83..f37d40b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,13 +1,26 @@ -import type { LoaderFunctionArgs, SessionData, TypedResponse } from '@remix-run/node'; -import { json, redirect } from '@remix-run/node'; +import { data, redirect, type LoaderFunctionArgs, type SessionData } from '@remix-run/node'; import { getAuthorizationUrl } from './get-authorization-url.js'; -import type { AccessToken, AuthKitLoaderOptions, AuthorizedData, Session, UnauthorizedData } from './interfaces.js'; +import type { + AccessToken, + AuthKitLoaderOptions, + AuthorizedData, + DataWithResponseInit, + Session, + UnauthorizedData, +} from './interfaces.js'; import { getWorkOS } from './workos.js'; import { sealData, unsealData } from 'iron-session'; import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; import { getConfig } from './config.js'; import { configureSessionStorage, getSessionStorage } from './sessionStorage.js'; +import { isResponse, isRedirect } from './utils.js'; + +// must be a type since this is a subtype of response +// interfaces must conform to the types they extend +export type TypedResponse = Response & { + json(): Promise; +}; async function updateSession(request: Request, debug: boolean) { const session = await getSessionFromCookie(request.headers.get('Cookie') as string); @@ -86,27 +99,119 @@ type AuthLoader = ( type AuthorizedAuthLoader = (args: LoaderFunctionArgs & { auth: AuthorizedData }) => LoaderReturnValue; +/** + * This loader handles authentication state, session management, and access token refreshing + * automatically, making it easier to build authenticated routes. + * + * Creates an authentication-aware loader function for React Router. + * + * This loader handles authentication state, session management, and access token refreshing + * automatically, making it easier to build authenticated routes. + * + * @overload + * Basic usage with enforced authentication that redirects unauthenticated users to sign in. + * + * @param loaderArgs - The loader arguments provided by React Router + * @param options - Configuration options with enforced sign-in + * + * @example + * export async function loader({ request }: LoaderFunctionArgs) { + * return authkitLoader( + * { request }, + * { ensureSignedIn: true } + * ); + * } + */ async function authkitLoader( loaderArgs: LoaderFunctionArgs, options: AuthKitLoaderOptions & { ensureSignedIn: true }, -): Promise>; - +): Promise>; + +/** + * This loader handles authentication state, session management, and access token refreshing + * automatically, making it easier to build authenticated routes. + * + * @overload + * Basic usage without enforced authentication, allowing both signed-in and anonymous users. + * + * @param loaderArgs - The loader arguments provided by React Router + * @param options - Optional configuration options + * + * @example + * export async function loader({ request }: LoaderFunctionArgs) { + * return authkitLoader({ request }); + * } + */ async function authkitLoader( loaderArgs: LoaderFunctionArgs, options?: AuthKitLoaderOptions, -): Promise>; - +): Promise>; + +/** + * This loader handles authentication state, session management, and access token refreshing + * automatically, making it easier to build authenticated routes. + * + * @overload + * Custom loader with enforced authentication, providing your own loader function + * that will only be called for authenticated users. + * + * @param loaderArgs - The loader arguments provided by React Router + * @param loader - A custom loader function that receives authentication data + * @param options - Configuration options with enforced sign-in + * + * @example + * export async function loader({ request }: LoaderFunctionArgs) { + * return authkitLoader( + * { request }, + * async ({ auth }) => { + * // This will only be called for authenticated users + * const userData = await fetchUserData(auth.accessToken); + * return { userData }; + * }, + * { ensureSignedIn: true } + * ); + * } + */ async function authkitLoader( loaderArgs: LoaderFunctionArgs, loader: AuthorizedAuthLoader, options: AuthKitLoaderOptions & { ensureSignedIn: true }, -): Promise>; - +): Promise>; + +/** + * This loader handles authentication state, session management, and access token refreshing + * automatically, making it easier to build authenticated routes. + * + * @overload + * Custom loader without enforced authentication, providing your own loader function + * that will be called for both authenticated and unauthenticated users. + * + * @param loaderArgs - The loader arguments provided by React Router + * @param loader - A custom loader function that receives authentication data + * @param options - Optional configuration options + * + * @example + * export async function loader({ request }: LoaderFunctionArgs) { + * return authkitLoader( + * { request }, + * async ({ auth }) => { + * if (auth.user) { + * // User is authenticated + * const userData = await fetchUserData(auth.accessToken); + * return { userData }; + * } else { + * // User is not authenticated + * return { publicData: await fetchPublicData() }; + * } + * } + * ); + * } + */ async function authkitLoader( loaderArgs: LoaderFunctionArgs, loader: AuthLoader, options?: AuthKitLoaderOptions, -): Promise>; +): Promise>; async function authkitLoader( loaderArgs: LoaderFunctionArgs, @@ -190,21 +295,21 @@ async function handleAuthLoader( session?: Session, ) { if (!loader) { - return json(auth, session ? { headers: { ...session.headers } } : undefined); + return data(auth, session ? { headers: { ...session.headers } } : undefined); } // If there's a custom loader, get the resulting data and return it with our // auth data plus session cookie header const loaderResult = await loader({ ...args, auth: auth as AuthorizedData }); - if (loaderResult instanceof Response) { + if (isResponse(loaderResult)) { // If the result is a redirect, return it unedited - if (loaderResult.status >= 300 && loaderResult.status < 400) { - return loaderResult; + if (isRedirect(loaderResult)) { + throw loaderResult; } const newResponse = new Response(loaderResult.body, loaderResult); - const data = await newResponse.json(); + const responseData = await newResponse.json(); // Set the content type in case the user returned a Response instead of the // json helper method @@ -213,12 +318,12 @@ async function handleAuthLoader( newResponse.headers.append('Set-Cookie', session.headers['Set-Cookie']); } - return json({ ...data, ...auth }, newResponse); + return data({ ...responseData, ...auth }, newResponse); } // If the loader returns a non-Response, assume it's a data object // istanbul ignore next - return json({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined); + return data({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined); } async function terminateSession(request: Request) { diff --git a/src/test-utils/test-helpers.ts b/src/test-utils/test-helpers.ts index bcf268c..64981d7 100644 --- a/src/test-utils/test-helpers.ts +++ b/src/test-utils/test-helpers.ts @@ -52,6 +52,8 @@ export function createAuthWithCodeResponse(overrides: Record = object: 'user' as const, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', + lastSignInAt: '2024-01-01T00:00:00Z', + externalId: null, }, ...overrides, }; diff --git a/src/utils.ts b/src/utils.ts index 73b807c..8e8b349 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import type { DataWithResponseInit } from './interfaces.js'; + /** * Returns a function that can only be called once. * Subsequent calls will return the result of the first call. @@ -16,3 +18,37 @@ export function lazy(fn: () => T): () => T { return result; }; } + +/** + * Returns true if the response is a redirect. + * @param res - The response to check. + * @returns True if the response is a redirect. + */ +export function isRedirect(res: Response) { + return res.status >= 300 && res.status < 400; +} + +/** + * Returns true if the response is a response. + * @param response - The response to check. + * @returns True if the response is a response. + */ +export function isResponse(response: unknown): response is Response { + return response instanceof Response; +} + +/** + * Checks if the data is a DataWithResponseInit object. + * @param data - The data to check. + * @returns True if the data is a DataWithResponseInit object. + */ +export function isDataWithResponseInit(data: unknown): data is DataWithResponseInit { + return ( + typeof data === 'object' && + data != null && + 'type' in data && + 'data' in data && + 'init' in data && + data.type === 'DataWithResponseInit' + ); +}