diff --git a/jest.config.mjs b/jest.config.mjs index 6574372..2c05f9d 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -11,6 +11,10 @@ export default { transform: { '^.+\\.[tj]sx?$': ['babel-jest', { configFile: './babel.config.mjs' }], }, + moduleNameMapper: { + '^@uvdsl/solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts', + '^@uvdsl/solid-oidc-client-browser/core$': '/test/mocks/solid-oidc-client-browser.ts', + }, setupFilesAfterEnv: ['./test/helpers/setup.ts'], testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], roots: ['/src', '/test'], diff --git a/package-lock.json b/package-lock.json index 41a86c2..f277202 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "4.0.7", "license": "MIT", "dependencies": { - "@inrupt/solid-client-authn-browser": "^4.0.0", + "@uvdsl/solid-oidc-client-browser": "^0.2.2", "solid-namespace": "^0.5.4" }, "devDependencies": { @@ -37,7 +37,7 @@ "node": ">=18" }, "peerDependencies": { - "rdflib": "^2.3.7" + "rdflib": "^2.3.9" } }, "node_modules/@asamuzakjp/css-color": { @@ -87,19 +87,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -392,12 +394,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1963,7 +1967,9 @@ } }, "node_modules/@discoveryjs/json-ext": { - "version": "1.0.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", "dev": true, "license": "MIT", "engines": { @@ -2278,45 +2284,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inrupt/oidc-client-ext": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@inrupt/oidc-client-ext/-/oidc-client-ext-4.0.0.tgz", - "integrity": "sha512-E32/yElFpADyWRFO6FdCyB1Ew1svsNX/fFdvHWP3qCBhSlfJVq2hMChWxs/RIRmTjHePyjT2UKEuItM09WXaWA==", - "license": "MIT", - "dependencies": { - "@inrupt/solid-client-authn-core": "^4.0.0", - "jose": "^5.1.3", - "oidc-client-ts": "^3.5.0", - "uuid": "^11.1.0" - } - }, - "node_modules/@inrupt/solid-client-authn-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@inrupt/solid-client-authn-browser/-/solid-client-authn-browser-4.0.0.tgz", - "integrity": "sha512-b7DpLMjYVMPiRv3QWqOmCeYqKL1t2THYQawuYM1zNqtN1SJGG5XEkXIy3ZQxx12tzAjeLNjH3ZAOg/CK/ehg2w==", - "license": "MIT", - "dependencies": { - "@inrupt/oidc-client-ext": "^4.0.0", - "@inrupt/solid-client-authn-core": "^4.0.0", - "events": "^3.3.0", - "jose": "^5.1.3", - "uuid": "^11.1.0" - } - }, - "node_modules/@inrupt/solid-client-authn-core": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@inrupt/solid-client-authn-core/-/solid-client-authn-core-4.0.0.tgz", - "integrity": "sha512-q4iur4TxEkhk9XaGAvyRP/+MjU1oBv2xlBdGE+uoXmDHAnIqUN71zZjCWZfZlyQFRETgH3OfZ9tPrNSDIPA/wg==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "jose": "^5.1.3", - "uuid": "^11.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3049,16 +3016,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", - "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.0", - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/typescript-estree": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -3074,14 +3041,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", - "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.0", - "@typescript-eslint/types": "^8.60.0", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -3096,14 +3063,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", - "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3114,9 +3081,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", - "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -3131,9 +3098,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", - "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -3145,16 +3112,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", - "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.0", - "@typescript-eslint/tsconfig-utils": "8.60.0", - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3173,9 +3140,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", "dev": true, "license": "ISC", "bin": { @@ -3186,13 +3153,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", - "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3479,6 +3446,15 @@ "win32" ] }, + "node_modules/@uvdsl/solid-oidc-client-browser": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@uvdsl/solid-oidc-client-browser/-/solid-oidc-client-browser-0.2.2.tgz", + "integrity": "sha512-JhcfSPu+eVyPMl2Dz46jq9ZHZwfZSqzCrQiHkvFZyam9ZEGXmLF1QJs4O+MddiEJaF5rVeEPd20YWprp5drLKw==", + "license": "MIT", + "dependencies": { + "jose": "^5.9.6" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "dev": true, @@ -5629,14 +5605,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -7876,15 +7844,6 @@ "license": "ISC", "peer": true }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -8434,18 +8393,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oidc-client-ts": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", - "integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==", - "license": "Apache-2.0", - "dependencies": { - "jwt-decode": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -9016,9 +8963,9 @@ } }, "node_modules/rdflib": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/rdflib/-/rdflib-2.3.7.tgz", - "integrity": "sha512-rpDq7AD8GrMO8aKu0FNoIfht2NNnIuP2JLGZvzBW+vfyRRU2HY0qHR9VHPB6udyIaPVAhUW/+QCcrEvbcglC1g==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/rdflib/-/rdflib-2.3.9.tgz", + "integrity": "sha512-6HnEQ22QzgqPW2/R8y5IaeQoXnho6U+ovU1q/ZF556zEnSK4buwhw8/CDdRDwIHZQh5+PAncQxUhluO3JmguJQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10078,9 +10025,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10242,9 +10189,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -10349,9 +10296,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.7", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", - "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.0.tgz", + "integrity": "sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10365,8 +10312,14 @@ "node": ">=12.0.0" }, "peerDependencies": { + "loader-utils": "*", "typescript": "*", - "webpack": "^5.0.0" + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } } }, "node_modules/ts-loader/node_modules/semver": { @@ -10747,19 +10700,6 @@ "license": "MIT", "peer": true }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -10886,17 +10826,16 @@ } }, "node_modules/webpack-cli": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.2.tgz", - "integrity": "sha512-dB0R4T+C/8YuvM+fabdvil6QE44/ChDXikV5lOOkrUeCkW5hTJv2pGLE3keh+D5hjYw8icBaJkZzpFoaHV4T+g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.3.tgz", + "integrity": "sha512-2E2C6A1e2El7791zQgTH7LPIuwLjRliow9OHS/qlJc9pwhZlCoL/uiwqd/1WSlXT83wJfmfDbkcqHXuXoPJZ3g==", "dev": true, "license": "MIT", "dependencies": { - "@discoveryjs/json-ext": "^1.0.0", + "@discoveryjs/json-ext": "^1.1.0", "commander": "^14.0.3", "cross-spawn": "^7.0.6", "envinfo": "^7.14.0", - "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", diff --git a/package.json b/package.json index 885be6f..e82e768 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build-dist": "webpack --progress", "postbuild-js": "rm -f dist/versionInfo.d.ts dist/versionInfo.d.ts.map", "lint": "eslint", + "lint-fix": "eslint --fix", "typecheck": "tsc --noEmit", "typecheck-test": "tsc --noEmit -p tsconfig.test.json", "test": "jest --no-coverage", @@ -72,10 +73,10 @@ "webpack-cli": "^7.0.2" }, "dependencies": { - "@inrupt/solid-client-authn-browser": "^4.0.0", + "@uvdsl/solid-oidc-client-browser": "^0.2.2", "solid-namespace": "^0.5.4" }, "peerDependencies": { - "rdflib": "^2.3.7" + "rdflib": "^2.3.9" } } diff --git a/src/authSession/authSession.ts b/src/authSession/authSession.ts index a125a97..554c4b1 100644 --- a/src/authSession/authSession.ts +++ b/src/authSession/authSession.ts @@ -1,7 +1,93 @@ -import { - Session, -} from '@inrupt/solid-client-authn-browser' +/** + * Auth session wiring. + * + * Takes the raw OIDC session from session.ts and layers on: + * - Login compatibility shim (normalises legacy call-site signatures + * and resolves the canonical issuer) + * - SessionEvents shim (legacy EventEmitter-style API) + * - Logout listener (emits 'logout' on session deactivation) + * + * Exports the fully assembled authSession. + */ -export const authSession = new Session() +import type { Session as OidcSession } from '@uvdsl/solid-oidc-client-browser/core' +import { _session } from './session' +import { resolveIssuerForLogin } from './issuer' +import { SessionEvents } from './events' +type SessionCompatibilityShape = { + webId?: string + isActive?: boolean + info?: { + webId?: string + isLoggedIn?: boolean + } + fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise + authFetch?: (input: string | URL | Request, init?: RequestInit, dpopPayload?: any) => Promise +} + +export type SessionWithLegacyEvents = OidcSession & SessionCompatibilityShape & { events: SessionEvents } + +// --------------------------------------------------------------------------- +// Login compatibility shim +// --------------------------------------------------------------------------- +// Wraps _session.login() so that call sites with different calling +// conventions all work. The underlying session expects: +// login(issuer: string, redirectUrl: string) +// +// idpOrOptions can be: +// - a string (issuer URL) — passed through with less resolution +// - an options object with any of these field-name variants: +// issuer: oidcIssuer | idp | issuer +// redirect: redirectUrl | redirect_uri | redirectUri +// (all redirect field names map to the same value: the URL the IdP +// should send the browser back to after authentication) +// - anything else — passed through to the underlying session as-is +// +// In all cases the issuer is resolved through +// /.well-known/openid-configuration before redirect so the canonical +// issuer host is used. + +const sessionAny = _session as any +const originalLogin = typeof sessionAny.login === 'function' + ? sessionAny.login.bind(_session) + : undefined + +if (originalLogin) { + sessionAny.login = async (idpOrOptions: any, redirectUri?: string) => { + if (idpOrOptions && typeof idpOrOptions === 'object' && !Array.isArray(idpOrOptions)) { + const oidcIssuer = idpOrOptions.oidcIssuer ?? idpOrOptions.idp ?? idpOrOptions.issuer + const redirectUrl = idpOrOptions.redirectUrl ?? idpOrOptions.redirect_uri ?? idpOrOptions.redirectUri + if (typeof oidcIssuer === 'string' && typeof redirectUrl === 'string') { + return originalLogin(await resolveIssuerForLogin(oidcIssuer), redirectUrl) + } + } + if (typeof idpOrOptions === 'string') { + return originalLogin(await resolveIssuerForLogin(idpOrOptions), redirectUri) + } + return originalLogin(idpOrOptions, redirectUri) + } +} + +// --------------------------------------------------------------------------- +// Legacy event layer +// --------------------------------------------------------------------------- + +const events = new SessionEvents() + +// Emit the legacy 'logout' event when the session transitions from active to inactive. +// 'login' and 'sessionRestore' are emitted in SolidAuthnLogic.checkUser() +// because only that call site knows which path activated the session. +let _wasActive = (_session as any).isActive ?? Boolean((_session as any).webId) +if (typeof (_session as unknown as EventTarget).addEventListener === 'function') { + ;(_session as unknown as EventTarget).addEventListener('sessionStateChange', () => { + const isNowActive = (_session as any).isActive ?? Boolean((_session as any).webId) + if (_wasActive && !isNowActive) { + events.emit('logout') + } + _wasActive = isNowActive + }) +} + +export const authSession: SessionWithLegacyEvents = Object.assign(_session, { events }) \ No newline at end of file diff --git a/src/authSession/events.ts b/src/authSession/events.ts new file mode 100644 index 0000000..8e7704a --- /dev/null +++ b/src/authSession/events.ts @@ -0,0 +1,35 @@ +/** + * Legacy event compatibility layer. + * + * Pure EventEmitter-style shim — no side effects, no uvdsl dependencies. + * Wired into the auth session by authSession.ts. + */ + +type LegacyEventName = 'login' | 'logout' | 'sessionRestore' +type LegacyEventHandler = (...args: unknown[]) => void + +/** + * Minimal EventEmitter-style shim so that existing consumers using + * `authSession.events.on('login' | 'logout' | 'sessionRestore', handler)` + * continue working without modification. + * + * Events are emitted by SolidAuthnLogic.checkUser() (login/sessionRestore) + * and by the sessionStateChange listener in authSession.ts (logout). + */ +export class SessionEvents { + private readonly listeners: Map> = new Map() + + on (event: LegacyEventName, handler: LegacyEventHandler): void { + if (!this.listeners.has(event)) this.listeners.set(event, new Set()) + this.listeners.get(event)!.add(handler) + } + + off (event: LegacyEventName, handler: LegacyEventHandler): void { + this.listeners.get(event)?.delete(handler) + } + + emit (event: LegacyEventName, ...args: unknown[]): void { + this.listeners.get(event)?.forEach(h => h(...args)) + } +} + diff --git a/src/authSession/issuer.ts b/src/authSession/issuer.ts new file mode 100644 index 0000000..1a3b69e --- /dev/null +++ b/src/authSession/issuer.ts @@ -0,0 +1,36 @@ +/** + * Issuer discovery utilities. + * + * Resolves OIDC issuer endpoints from /.well-known/openid-configuration + * so that login can use the canonical issuer host. + */ + +async function discoverIssuerFromWellKnown (issuer: string): Promise { + try { + const issuerUrl = new URL(issuer) + const wellKnownUrl = new URL('/.well-known/openid-configuration', issuerUrl.origin) + const wellKnownResponse = await fetch(wellKnownUrl.toString(), { credentials: 'include' }) + if (!wellKnownResponse.ok) { + return null + } + + const wellKnownPayload = await wellKnownResponse.json() + if (typeof wellKnownPayload?.issuer !== 'string' || !wellKnownPayload.issuer) { + return null + } + + return wellKnownPayload.issuer.replace(/\/$/, '') + } catch (_err) { + return null + } +} + +export async function resolveIssuerForLogin (issuer: string): Promise { + // Prefer the issuer advertised by discovery; if app and issuer hosts still differ, + // redirecting to the canonical issuer host is cleaner than rewriting the issuer here. + const discoveredIssuer = await discoverIssuerFromWellKnown(issuer) + if (discoveredIssuer) { + return discoveredIssuer + } + return issuer +} diff --git a/src/authSession/session.ts b/src/authSession/session.ts new file mode 100644 index 0000000..a7325fa --- /dev/null +++ b/src/authSession/session.ts @@ -0,0 +1,221 @@ +/** + * OIDC session factory. + * + * Everything needed to create and configure the underlying auth session: + * - Session database backends (in-memory and IndexedDB) + * - Session instantiation (WebSession or SessionCore, with local-dev + * fallbacks for environments where the service worker can't load) + * - Login compatibility shim that normalises legacy call-site signatures + * and resolves the canonical issuer through /.well-known discovery + * + * The raw session instance (_session) is consumed by authSession.ts where + * the legacy event layer and logout listener are attached. + * + * DO NOT import _session directly — always go through authSession.ts so + * the event wiring is guaranteed to run. + */ + +import { + Session as WebSession, +} from '@uvdsl/solid-oidc-client-browser' +import * as OidcCore from '@uvdsl/solid-oidc-client-browser/core' +import type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core' + +// --------------------------------------------------------------------------- +// Session databases +// --------------------------------------------------------------------------- + +export class MemorySessionDatabase implements SessionDatabase { + private readonly map = new Map() + + private shouldPreserveExistingRefreshToken(id: string, value: any): boolean { + return id === 'refresh_token' && (value == null || value === '') && this.map.has(id) + } + + async init (): Promise { + return this + } + + async setItem (id: string, value: any): Promise { + // Some Solid IdPs do not include refresh_token on refresh responses. + // Keep the previous token instead of overwriting it with null/undefined. + if (this.shouldPreserveExistingRefreshToken(id, value)) { + return + } + this.map.set(id, value) + } + + async getItem (id: string): Promise { + return this.map.has(id) ? this.map.get(id) : null + } + + async deleteItem (id: string): Promise { + this.map.delete(id) + } + + async clear (): Promise { + this.map.clear() + } + + close (): void { + // No-op for in-memory database + } +} + +export class IndexedDbSessionDatabase implements SessionDatabase { + private db: IDBDatabase | null = null + private readonly dbName = 'soidc' + private readonly storeName = 'session' + private readonly dbVersion = 1 + + private async shouldPreserveExistingRefreshToken(id: string, value: any): Promise { + if (id !== 'refresh_token' || !(value == null || value === '')) { + return false + } + const existing = await this.getItem(id) + return existing != null && existing !== '' + } + + async init (): Promise { + if (this.db) return this + if (typeof indexedDB === 'undefined') { + throw new Error('IndexedDB is not available in this environment') + } + + await new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + this.db = request.result + resolve() + } + request.onupgradeneeded = () => { + const db = request.result + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName) + } + } + }) + + return this + } + + async setItem (id: string, value: any): Promise { + await this.init() + // Some Solid IdPs do not include refresh_token on refresh responses. + // Keep the previous token instead of overwriting it with null/undefined. + if (await this.shouldPreserveExistingRefreshToken(id, value)) { + return + } + await this.withStore('readwrite', store => store.put(value, id)) + } + + async getItem (id: string): Promise { + await this.init() + return this.withStore('readonly', store => store.get(id)) + } + + async deleteItem (id: string): Promise { + await this.init() + await this.withStore('readwrite', store => store.delete(id)) + } + + async clear (): Promise { + await this.init() + await this.withStore('readwrite', store => store.clear()) + } + + close (): void { + if (this.db) { + this.db.close() + this.db = null + } + } + + private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest): Promise { + return new Promise((resolve, reject) => { + if (!this.db) { + reject(new Error('Session database not initialized')) + return + } + + const tx = this.db.transaction(this.storeName, mode) + const store = tx.objectStore(this.storeName) + const request = op(store) + let result: any = null + + tx.onerror = () => reject(tx.error ?? request.error) + tx.onabort = () => reject(tx.error ?? request.error ?? new Error('IndexedDB transaction aborted')) + tx.oncomplete = () => resolve(result) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + result = request.result ?? null + } + }) + } +} + +// --------------------------------------------------------------------------- +// Session instantiation +// --------------------------------------------------------------------------- + +function getSessionCoreCtor (): (new (...args: any[]) => OidcSession) | null { + const coreAny = OidcCore as any + const candidate = coreAny.SessionCore ?? coreAny.default?.SessionCore ?? coreAny.default + + if (typeof candidate !== 'function') { + return null + } + + return candidate as new (...args: any[]) => OidcSession +} + +const SessionCoreCtor = getSessionCoreCtor() + +function createSession (): OidcSession { + const shouldSkipWorkerInLocalDev = typeof window !== 'undefined' && (() => { + const host = window.location.hostname + // In local NSS setups (including subdomain mode like alice.localhost), + // worker-based session storage can be brittle and lose state on reload. + // Prefer SessionCore + IndexedDB for deterministic persistence. + return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost') + })() + + if (shouldSkipWorkerInLocalDev) { + if (SessionCoreCtor) { + return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() }) + } + return new WebSession() + } + + try { + return new WebSession() + } catch (error) { + // In some deployments, worker URL resolution can become file:// and fail cross-origin. + // Fall back to SessionCore so auth still works without background refresh worker. + // Use IndexedDB to keep refresh-token persistence across page reloads. + console.warn('solid-logic: falling back to non-worker auth session:', error) + try { + if (SessionCoreCtor) { + return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() }) + } + return new WebSession() + } catch (dbError) { + console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError) + if (SessionCoreCtor) { + return new SessionCoreCtor(undefined, { database: new MemorySessionDatabase() }) + } + return new WebSession() + } + } +} + +// --------------------------------------------------------------------------- +// Singleton session +// --------------------------------------------------------------------------- + +const _session = createSession() + +export { _session } diff --git a/src/authn/SolidAuthnLogic.ts b/src/authn/SolidAuthnLogic.ts index 6d49a8e..6aa198c 100644 --- a/src/authn/SolidAuthnLogic.ts +++ b/src/authn/SolidAuthnLogic.ts @@ -1,26 +1,38 @@ import { namedNode, NamedNode, sym } from 'rdflib' import { appContext, offlineTestID } from './authUtil' import * as debug from '../util/debug' -import { EVENTS, Session } from '@inrupt/solid-client-authn-browser' -import { AuthenticationContext, AuthnLogic } from '../types' +import type { SessionWithLegacyEvents } from '../authSession/authSession' +import type { AuthenticationContext, AuthnLogic } from '../types' export class SolidAuthnLogic implements AuthnLogic { - private session: Session + private session: SessionWithLegacyEvents + private checkUserInFlight: Promise | null = null + private sessionRestoreHookAttached = false + private fallbackWebId: string | null = null - constructor(solidAuthSession: Session) { + constructor(solidAuthSession: SessionWithLegacyEvents) { this.session = solidAuthSession } // we created authSession getter because we want to access it as authn.authSession externally - get authSession():Session { return this.session } + get authSession(): SessionWithLegacyEvents { return this.session } currentUser(): NamedNode | null { const app = appContext() if (app.viewingNoAuthPage) { return sym(app.webId) } - if (this && this.session && this.session.info && this.session.info.webId && this.session.info.isLoggedIn) { - return sym(this.session.info.webId) + const sessionAny = this.session as any + const infoWebId = sessionAny?.info?.webId + const sessionWebId = sessionAny?.webId + const webId = infoWebId || sessionWebId || this.fallbackWebId + const infoLoggedIn = sessionAny?.info?.isLoggedIn + const sessionActive = sessionAny?.isActive + const isLoggedIn = infoLoggedIn === true || sessionActive === true || + ((infoLoggedIn == null && sessionActive == null) ? Boolean(webId) : false) || + Boolean(this.fallbackWebId) + if (this && this.session && webId && isLoggedIn) { + return sym(webId) } return offlineTestID() // null unless testing } @@ -40,21 +52,72 @@ export class SolidAuthnLogic implements AuthnLogic { if (preLoginRedirectHash) { window.localStorage.setItem('preLoginRedirectHash', preLoginRedirectHash) } - this.session.events.on(EVENTS.SESSION_RESTORED, (url) => { - debug.log(`Session restored to ${url}`) - if (document.location.toString() !== url) history.replaceState(null, '', url) - }) + const sessionAny = this.session as any + if (!this.sessionRestoreHookAttached && typeof sessionAny?.events?.on === 'function') { + // Backward-compatible hook for auth clients exposing an EventEmitter-style API. + sessionAny.events.on('sessionRestore', (url: string) => { + debug.log(`Session restored to ${url}`) + if (document.location.toString() !== url) history.replaceState(null, '', url) + }) + this.sessionRestoreHookAttached = true + } + + if (!this.checkUserInFlight) { + this.checkUserInFlight = this.resolveCurrentUser() + } + + const inFlight = this.checkUserInFlight + let me: NamedNode | null + try { + me = await inFlight + } finally { + if (this.checkUserInFlight === inFlight) { + this.checkUserInFlight = null + } + } + + return Promise.resolve(setUserCallback ? setUserCallback(me) : me) + } + + private async resolveCurrentUser (): Promise { + const sessionAny = this.session as any /** * Handle a successful authentication redirect */ const redirectUrl = new URL(window.location.href) redirectUrl.hash = '' - await this.session - .handleIncomingRedirect({ + if (typeof sessionAny?.handleIncomingRedirect === 'function') { + await sessionAny.handleIncomingRedirect({ restorePreviousSession: true, url: redirectUrl.href }) + } else { + if (typeof sessionAny?.restore === 'function') { + const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId) + try { + await sessionAny.restore() + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (!/no session to restore/i.test(message)) { + throw error + } + debug.log('No previous session to restore') + } + const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId) + if (!wasActive && isNowActive) { + sessionAny.events?.emit('sessionRestore', window.location.href) + } + } + if (typeof sessionAny?.handleRedirectFromLogin === 'function') { + const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId) + await sessionAny.handleRedirectFromLogin() + const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId) + if (!wasActive && isNowActive) { + sessionAny.events?.emit('login') + } + } + } // Check to see if a hash was stored in local storage const postLoginRedirectHash = window.localStorage.getItem('preLoginRedirectHash') @@ -78,10 +141,21 @@ export class SolidAuthnLogic implements AuthnLogic { // Check to see if already logged in / have the WebID let me = offlineTestID() if (me) { - return Promise.resolve(setUserCallback ? setUserCallback(me) : me) + return me + } + + let webId = this.webIdFromSession(sessionAny?.info, sessionAny) + if (!webId) { + // NSS-specific fallback: recover WebID from NSS cookie session when client restore is empty. + webId = await this.probeNssCookieBackedWebId() + } + + if (webId) { + this.fallbackWebId = webId + } else { + this.fallbackWebId = null } - const webId = this.webIdFromSession(this.session.info) if (webId) { me = this.saveUser(webId) } @@ -90,7 +164,41 @@ export class SolidAuthnLogic implements AuthnLogic { debug.log(`(Logged in as ${me} by authentication)`) } - return Promise.resolve(setUserCallback ? setUserCallback(me) : me) + return me + } + + private async probeNssCookieBackedWebId (): Promise { + if (typeof window === 'undefined') { + return null + } + + const { hostname, port, protocol } = window.location + const localhostSuffix = '.localhost' + // NSS local pods use subdomains like alice.localhost. + if (!hostname.endsWith(localhostSuffix)) { + return null + } + + const podName = hostname.slice(0, -localhostSuffix.length) + if (!podName || podName === 'localhost' || podName.includes('.')) { + return null + } + + try { + // NSS returns 403 on this account page when the cookie session is valid. + const probeResponse = await fetch('/account/password/change', { + credentials: 'include', + redirect: 'manual', + cache: 'no-store' + }) + if (probeResponse.status !== 403) { + return null + } + const origin = `${protocol}//${hostname}${port ? `:${port}` : ''}` + return `${origin}/profile/card#me` + } catch (_error) { + return null + } } /** @@ -119,8 +227,23 @@ export class SolidAuthnLogic implements AuthnLogic { /** * @returns {Promise} Resolves with WebID URI or null */ - webIdFromSession (session?: { webId?: string, isLoggedIn: boolean }): string | null { - const webId = session?.webId && session.isLoggedIn ? session.webId : null + webIdFromSession ( + sessionInfo?: { webId?: string, isLoggedIn?: boolean }, + sessionRoot?: { webId?: string, isLoggedIn?: boolean, isActive?: boolean } + ): string | null { + const webId = sessionInfo?.webId || sessionRoot?.webId + if (!webId) { + return null + } + const infoLoggedIn = sessionInfo?.isLoggedIn + const rootLoggedIn = sessionRoot?.isLoggedIn + const rootActive = sessionRoot?.isActive + if (infoLoggedIn === true || rootLoggedIn === true || rootActive === true) { + return webId + } + if (infoLoggedIn === false && rootLoggedIn === false && rootActive === false) { + return null + } return webId } diff --git a/src/authn/serverLogout.ts b/src/authn/serverLogout.ts new file mode 100644 index 0000000..6853008 --- /dev/null +++ b/src/authn/serverLogout.ts @@ -0,0 +1,43 @@ +export type ServerLogoutOptions = { + issuer?: string + postLogoutRedirectPath?: string +} + +export async function performServerSideLogout (options: ServerLogoutOptions = {}): Promise { + if (typeof window === 'undefined') { + return false + } + const issuer = options.issuer || '' + const postLogoutRedirectPath = options.postLogoutRedirectPath || '/' + + // Provider-specific logout endpoint discovery (OIDC end_session_endpoint). + try { + if (issuer) { + const wellKnownUri = new URL(issuer) + wellKnownUri.pathname = '/.well-known/openid-configuration' + const wellKnownResult = await fetch(wellKnownUri.toString(), { credentials: 'include' }) + + if (wellKnownResult.status === 200) { + const openidConfiguration = await wellKnownResult.json() + if (openidConfiguration && openidConfiguration.end_session_endpoint) { + await fetch(openidConfiguration.end_session_endpoint, { credentials: 'include' }) + } + } + } + } catch (_err) { + // Continue with local logout even if provider logout is unavailable. + } + + // NSS well-known logout endpoint clears cookie-backed server sessions. + try { + const logoutResponse = await fetch('/.well-known/solid/logout', { credentials: 'include' }) + if (logoutResponse.ok || logoutResponse.redirected) { + window.location.assign(postLogoutRedirectPath) + return true + } + } catch (_err) { + // Not all deployments expose this endpoint. + } + + return false +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9095ef1..5cbb29c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ const store = solidLogicSingleton.store export { ACL_LINK } from './acl/aclLogic' export { offlineTestID, appContext } from './authn/authUtil' +export { performServerSideLogout } from './authn/serverLogout' export { getSuggestedIssuers } from './issuer/issuerLogic' export { createTypeIndexLogic } from './typeIndex/typeIndexLogic' export type { AppDetails, SolidNamespace, AuthenticationContext, SolidLogic, ChatLogic } from './types' diff --git a/src/issuer/issuerLogic.ts b/src/issuer/issuerLogic.ts index eea468b..02ecd3e 100644 --- a/src/issuer/issuerLogic.ts +++ b/src/issuer/issuerLogic.ts @@ -27,14 +27,9 @@ export function getSuggestedIssuers (): { name: string, uri: string }[] { // Suggest the current host if not already included const { host, origin } = new URL(location.href) const hosts = issuers.map(({ uri }) => new URL(uri).host) - if (!hosts.includes(host) && !hosts.some(existing => isSubdomainOf(host, existing))) { + if (!hosts.includes(host)) { issuers.unshift({ name: host, uri: origin }) } return issuers - } - -function isSubdomainOf (subdomain: string, domain: string): boolean { - const dot = subdomain.length - domain.length - 1 - return dot > 0 && subdomain[dot] === '.' && subdomain.endsWith(domain) -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/logic/solidLogic.ts b/src/logic/solidLogic.ts index 9c62391..11621d1 100644 --- a/src/logic/solidLogic.ts +++ b/src/logic/solidLogic.ts @@ -1,15 +1,15 @@ -import { Session } from '@inrupt/solid-client-authn-browser' import * as rdf from 'rdflib' import { LiveStore, NamedNode, Statement } from 'rdflib' import { createAclLogic } from '../acl/aclLogic' import { SolidAuthnLogic } from '../authn/SolidAuthnLogic' +import type { SessionWithLegacyEvents } from '../authSession/authSession' import { createChatLogic } from '../chat/chatLogic' import { createInboxLogic } from '../inbox/inboxLogic' import { createProfileLogic } from '../profile/profileLogic' import { createTypeIndexLogic } from '../typeIndex/typeIndexLogic' import { createContainerLogic } from '../util/containerLogic' import { createUtilityLogic } from '../util/utilityLogic' -import { AuthnLogic, SolidLogic } from '../types' +import type { AuthnLogic, SolidLogic } from '../types' import * as debug from '../util/debug' /* ** It is important to distinquish `fetch`, a function provided by the browser @@ -17,7 +17,7 @@ import * as debug from '../util/debug' ** into a `ConnectedStore` or a `LiveStore`. A Fetcher object is ** available at store.fetcher, and `fetch` function at `store.fetcher._fetch`, */ -export function createSolidLogic(specialFetch: { fetch: (url: any, requestInit: any) => any }, session: Session): SolidLogic { +export function createSolidLogic(specialFetch: { fetch: (url: any, requestInit: any) => any }, session: SessionWithLegacyEvents): SolidLogic { debug.log('SolidLogic: Unique instance created. There should only be one of these.') const store: LiveStore = rdf.graph() as LiveStore diff --git a/src/logic/solidLogicSingleton.ts b/src/logic/solidLogicSingleton.ts index fed3e23..8320b6f 100644 --- a/src/logic/solidLogicSingleton.ts +++ b/src/logic/solidLogicSingleton.ts @@ -5,9 +5,17 @@ import { SolidLogic } from '../types' const _fetch = async (url, requestInit) => { const omitCreds = requestInit && requestInit.credentials && requestInit.credentials == 'omit' - if (authSession.info.webId && !omitCreds) { // see https://github.com/solidos/solidos/issues/114 + const sessionAny = authSession as any + const sessionWebId = sessionAny?.info?.webId || sessionAny?.webId + if (sessionWebId && !omitCreds) { // see https://github.com/solidos/solidos/issues/114 // In fact fetch should respect credentials omit itself - return authSession.fetch(url, requestInit) + const authenticatedFetch = (typeof sessionAny.fetch === 'function') + ? sessionAny.fetch.bind(sessionAny) + : (typeof sessionAny.authFetch === 'function' ? sessionAny.authFetch.bind(sessionAny) : null) + if (authenticatedFetch) { + return authenticatedFetch(url, requestInit) + } + return window.fetch(url, requestInit) } else { return window.fetch(url, requestInit) } diff --git a/src/types.ts b/src/types.ts index 62a6585..8faa83a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Session } from '@inrupt/solid-client-authn-browser' +import type { SessionWithLegacyEvents } from './authSession/authSession' import { LiveStore, NamedNode, Statement } from 'rdflib' export type AppDetails = { @@ -21,7 +21,7 @@ export type AuthenticationContext = { } export interface AuthnLogic { - authSession: Session //this needs to be deprecated in the future. Is only here to allow imports like panes.UI.authn.authSession prior to moving authn from ui to logic + authSession: SessionWithLegacyEvents //this needs to be deprecated in the future. Is only here to allow imports like panes.UI.authn.authSession prior to moving authn from ui to logic currentUser: () => NamedNode | null checkUser: (setUserCallback?: (me: NamedNode | null) => T) => Promise saveUser: (webId: NamedNode | string | null, diff --git a/test/logic.test.ts b/test/logic.test.ts index 5cdb548..e406f92 100644 --- a/test/logic.test.ts +++ b/test/logic.test.ts @@ -1,4 +1,6 @@ import { solidLogicSingleton } from '../src/logic/solidLogicSingleton' +import { authSession } from '../src/authSession/authSession' +import fetchMock from 'jest-fetch-mock' import { silenceDebugMessages } from './helpers/debugger' silenceDebugMessages() @@ -27,3 +29,58 @@ describe('authn', () => { }) }) +describe('solidLogicSingleton fetch bridge', () => { + const singletonFetch = (solidLogicSingleton.store.fetcher as any)._fetch as (url: string, init?: RequestInit) => Promise + + let originalFetch: any + let originalAuthFetch: any + let originalWebId: any + let originalInfo: any + + beforeEach(() => { + fetchMock.resetMocks() + + const sessionAny = authSession as any + originalFetch = sessionAny.fetch + originalAuthFetch = sessionAny.authFetch + originalWebId = sessionAny.webId + originalInfo = sessionAny.info + + sessionAny.webId = undefined + sessionAny.info = { isLoggedIn: false } + }) + + afterEach(() => { + const sessionAny = authSession as any + sessionAny.fetch = originalFetch + sessionAny.authFetch = originalAuthFetch + sessionAny.webId = originalWebId + sessionAny.info = originalInfo + }) + + it('uses window.fetch when credentials are omit even if a session exists', async () => { + const sessionAny = authSession as any + sessionAny.webId = 'https://alice.example/profile#me' + sessionAny.fetch = jest.fn().mockResolvedValue(new Response('session')) + + fetchMock.mockResponseOnce('window') + + await singletonFetch('https://example.com/resource', { credentials: 'omit' }) + + expect(sessionAny.fetch).not.toHaveBeenCalled() + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it('falls back to authFetch when session.fetch is unavailable', async () => { + const sessionAny = authSession as any + sessionAny.webId = 'https://alice.example/profile#me' + sessionAny.fetch = undefined + sessionAny.authFetch = jest.fn().mockResolvedValue(new Response('auth')) + + await singletonFetch('https://example.com/resource') + + expect(sessionAny.authFetch).toHaveBeenCalledTimes(1) + expect(fetchMock).not.toHaveBeenCalled() + }) +}) + diff --git a/test/mocks/solid-oidc-client-browser.ts b/test/mocks/solid-oidc-client-browser.ts new file mode 100644 index 0000000..76668a0 --- /dev/null +++ b/test/mocks/solid-oidc-client-browser.ts @@ -0,0 +1,85 @@ +type Listener = (...args: any[]) => void + +class EventEmitterLike { + private listeners: Record = {} + + on(event: string, listener: Listener): void { + const list = this.listeners[event] || [] + list.push(listener) + this.listeners[event] = list + } + + emit(event: string, ...args: any[]): void { + const list = this.listeners[event] || [] + list.forEach(listener => listener(...args)) + } +} + +export class Session { + info: { webId?: string, isLoggedIn: boolean } = { isLoggedIn: false } + webId?: string + isActive = false + events = new EventEmitterLike() + + async handleIncomingRedirect(): Promise { + return + } + + async handleRedirectFromLogin(): Promise { + return + } + + async restore(): Promise { + return + } + + async login(): Promise { + return + } + + async logout(): Promise { + this.info = { isLoggedIn: false } + this.webId = undefined + this.isActive = false + } + + fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return globalThis.fetch(input, init) + } + + authFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return globalThis.fetch(input, init) + } +} + +export class SessionCore extends Session { + constructor(_clientDetails?: unknown, _sessionOptions?: unknown) { + super() + } +} + +export class SessionIDB { + async init(): Promise { + return this + } + + async setItem(_id: string, _value: any): Promise { + return + } + + async getItem(_id: string): Promise { + return null + } + + async deleteItem(_id: string): Promise { + return + } + + async clear(): Promise { + return + } + + close(): void { + return + } +} diff --git a/test/solidAuthLogic.test.ts b/test/solidAuthLogic.test.ts index 4d1d633..b0132a2 100644 --- a/test/solidAuthLogic.test.ts +++ b/test/solidAuthLogic.test.ts @@ -10,6 +10,20 @@ import { AuthenticationContext } from '../src/types' silenceDebugMessages() let solidAuthnLogic +jest.mock('../src/authSession/authSession', () => { + const EventEmitter = require('events') + const authSession = { + events: new EventEmitter(), + addEventListener: function (event, listener) { + this.events.on(event, listener) + }, + removeEventListener: function (event, listener) { + this.events.off(event, listener) + }, + } + return { authSession } +}) + describe('SolidAuthnLogic', () => { beforeEach(() => {