diff --git a/config/dev-http-subdomain.json b/config/dev-http-subdomain.json index 19abb8a..599cc0e 100644 --- a/config/dev-http-subdomain.json +++ b/config/dev-http-subdomain.json @@ -36,7 +36,8 @@ "css:config/util/representation-conversion/default.json", "css:config/util/resource-locker/file.json", "css:config/util/variables/default.json", - "pivot:config/pivot-overrides.json" + "pivot:config/pivot-overrides.json", + "pivot:config/pivot-file-locker-overrides.json" ], "@graph": [ { diff --git a/config/dev-http-suffix.json b/config/dev-http-suffix.json index fb5fa89..3b1f8f1 100644 --- a/config/dev-http-suffix.json +++ b/config/dev-http-suffix.json @@ -36,7 +36,8 @@ "css:config/util/representation-conversion/default.json", "css:config/util/resource-locker/file.json", "css:config/util/variables/default.json", - "pivot:config/pivot-overrides.json" + "pivot:config/pivot-overrides.json", + "pivot:config/pivot-file-locker-overrides.json" ], "@graph": [ { diff --git a/config/pivot-file-locker-overrides.json b/config/pivot-file-locker-overrides.json new file mode 100644 index 0000000..5dcb487 --- /dev/null +++ b/config/pivot-file-locker-overrides.json @@ -0,0 +1,20 @@ +{ + "comment": "Overrides specific to configs that use the file-system resource locker. Imported only by prod/dev configs, NOT by test.json (which uses the memory locker).", + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/pivot/^1.0.0/components/context.jsonld" + ], + "@graph": [ + { + "comment": "Bound the lock acquisition retries so a contended lock fails fast instead of spinning the event loop and filesystem threadpool forever (~30s max wait).", + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:FileSystemResourceLocker" }, + "overrideParameters": { + "@type": "FileSystemResourceLocker", + "attemptSettings_retryCount": 600, + "attemptSettings_retryDelay": 50, + "attemptSettings_retryJitter": 30 + } + } + ] +} diff --git a/config/pivot-overrides.json b/config/pivot-overrides.json index 0abcd00..99a07e8 100644 --- a/config/pivot-overrides.json +++ b/config/pivot-overrides.json @@ -61,6 +61,82 @@ "templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" }, "template": "./templates/main.html.ejs" } + }, + { + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:AccountStorage" }, + "overrideParameters": { + "@type": "SafeBaseLoginAccountStorage", + "storage": { "@id": "urn:solid-server:default:IndexedStorage" } + } + }, + { + "comment": "Scope the cookie sweep to its own container so entries()/sweeps do not walk the entire /.internal/ tree (sustained high CPU with many accounts).", + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:CookieStorage" }, + "overrideParameters": { + "@type": "WrappedExpiringStorage", + "source": { + "@type": "MaxKeyLengthStorage", + "source": { + "@type": "JsonResourceStorage", + "source": { "@id": "urn:solid-server:default:ResourceStore_Backend" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "container": "/.internal/accounts/cookies/" + } + } + } + }, + { + "comment": "Scope the forgot-password sweep to its own container.", + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:ForgotPasswordStorage" }, + "overrideParameters": { + "@type": "WrappedExpiringStorage", + "source": { + "@type": "MaxKeyLengthStorage", + "source": { + "@type": "JsonResourceStorage", + "source": { "@id": "urn:solid-server:default:ResourceStore_Backend" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "container": "/.internal/accounts/forgot-password/" + } + } + } + }, + { + "comment": "Scope the token sweep to its own container.", + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:ExpiringTokenStorage" }, + "overrideParameters": { + "@type": "WrappedExpiringStorage", + "source": { + "@type": "MaxKeyLengthStorage", + "source": { + "@type": "JsonResourceStorage", + "source": { "@id": "urn:solid-server:default:ResourceStore_Backend" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "container": "/.internal/idp/tokens/" + } + } + } + }, + { + "comment": "Scope the adapter sweep to its own container. Requires the @id added to the installed CSS adapter-factory.json by the patch-package override.", + "@type": "Override", + "overrideInstance": { "@id": "urn:solid-server:default:ExpiringAdapterStorage" }, + "overrideParameters": { + "@type": "WrappedExpiringStorage", + "source": { + "@type": "MaxKeyLengthStorage", + "source": { + "@type": "JsonResourceStorage", + "source": { "@id": "urn:solid-server:default:ResourceStore_Backend" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "container": "/.internal/idp/adapter/" + } + } + } } ] } diff --git a/config/prod.json b/config/prod.json index 5e8cf62..78802f5 100644 --- a/config/prod.json +++ b/config/prod.json @@ -35,7 +35,8 @@ "css:config/util/representation-conversion/default.json", "css:config/util/resource-locker/file.json", "css:config/util/variables/default.json", - "pivot:config/pivot-overrides.json" + "pivot:config/pivot-overrides.json", + "pivot:config/pivot-file-locker-overrides.json" ], "@graph": [ { diff --git a/config/suffix.json b/config/suffix.json index cf12ef6..79c6889 100644 --- a/config/suffix.json +++ b/config/suffix.json @@ -35,7 +35,8 @@ "css:config/util/representation-conversion/default.json", "css:config/util/resource-locker/file.json", "css:config/util/variables/default.json", - "pivot:config/pivot-overrides.json" + "pivot:config/pivot-overrides.json", + "pivot:config/pivot-file-locker-overrides.json" ], "@graph": [ { diff --git a/package-lock.json b/package-lock.json index 3d53390..2ee345f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@solid/pivot", "version": "1.8.2", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@inrupt/solid-client-authn-core": "^3.1.1", @@ -3132,6 +3133,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3149,6 +3151,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3161,6 +3164,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3173,12 +3177,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -3196,6 +3202,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -3211,6 +3218,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -3804,6 +3812,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5765,6 +5774,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, "funding": [ { "type": "github", @@ -6484,6 +6494,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -7234,6 +7245,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -7250,6 +7262,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -7478,6 +7491,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -7510,6 +7524,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7519,6 +7534,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -8309,6 +8325,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -9087,6 +9104,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -9920,6 +9938,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -12160,6 +12179,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-json-path": { @@ -12455,6 +12475,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -14080,6 +14101,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14106,6 +14128,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14884,6 +14907,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", diff --git a/package.json b/package.json index 8b445c0..828ee0b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build:components": "componentsjs-generator -s src -c dist/components -i .componentsignore -r pivot", "build:ts": "tsc", "prepare": "npm run build", + "postinstall": "patch-package", "test": "jest" }, "files": [ diff --git a/patches/@solid+community-server+7.2.0.patch b/patches/@solid+community-server+7.2.0.patch new file mode 100644 index 0000000..0307c59 --- /dev/null +++ b/patches/@solid+community-server+7.2.0.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@solid/community-server/config/identity/handler/base/adapter-factory.json b/node_modules/@solid/community-server/config/identity/handler/base/adapter-factory.json +index c2b9890..c406ba6 +--- a/node_modules/@solid/community-server/config/identity/handler/base/adapter-factory.json ++++ b/node_modules/@solid/community-server/config/identity/handler/base/adapter-factory.json +@@ -13,6 +13,7 @@ + "source": { + "@type": "ExpiringAdapterFactory", + "storage": { ++ "@id": "urn:solid-server:default:ExpiringAdapterStorage", + "@type": "WrappedExpiringStorage", + "source": { + "@type": "ContainerPathStorage", diff --git a/patches/proper-lockfile+4.1.2.patch b/patches/proper-lockfile+4.1.2.patch new file mode 100644 index 0000000..e2a8b2d --- /dev/null +++ b/patches/proper-lockfile+4.1.2.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/proper-lockfile/lib/lockfile.js b/node_modules/proper-lockfile/lib/lockfile.js +index 97b6637..25bd0a2 100644 +--- a/node_modules/proper-lockfile/lib/lockfile.js ++++ b/node_modules/proper-lockfile/lib/lockfile.js +@@ -99,6 +99,13 @@ function removeLock(file, options, callback) { + function updateLock(file, options) { + const lock = locks[file]; + ++ // Guard against a renewal timer racing with lock release: ++ // `locks[file]` may already be deleted when an in-flight fs callback recurses. ++ /* istanbul ignore if */ ++ if (!lock) { ++ return; ++ } ++ + // Just for safety, should never happen + /* istanbul ignore if */ + if (lock.updateTimeout) { diff --git a/src/identity/interaction/account/util/SafeBaseLoginAccountStorage.ts b/src/identity/interaction/account/util/SafeBaseLoginAccountStorage.ts new file mode 100644 index 0000000..05ba222 --- /dev/null +++ b/src/identity/interaction/account/util/SafeBaseLoginAccountStorage.ts @@ -0,0 +1,47 @@ +import { + ACCOUNT_TYPE, + BaseLoginAccountStorage, + createErrorMessage, + getLoggerFor, +} from '@solid/community-server'; + +const LOGIN_COUNT = 'linkedLoginsCount'; + +/** + * A {@link BaseLoginAccountStorage} that prevents the periodic cleanup of accounts + * without login methods from crashing the process when a lock timeout occurs. + * + * The storage is typed as `any` (and the class is not generic) because the + * componentsjs-generator cannot resolve the `IndexedStorage`/`IndexTypeCollection` + * generic types from the installed Community Solid Server `.d.ts` (TS2415-safe + * `logger` is renamed to `safeLogger` for the same reason). + */ +export class SafeBaseLoginAccountStorage extends BaseLoginAccountStorage { + // Renamed (not `logger`) because the base class already declares a private `logger`, + // which cannot be re-declared in a subclass (TS2415). + private readonly safeLogger = getLoggerFor(this); + + private readonly timeout: number; + + public constructor(storage: any, expiration = 30 * 60) { + super(storage, expiration); + this.timeout = expiration * 1000; + } + + protected createAccountTimeout(id: string): void { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const timer = setTimeout(async(): Promise => { + try { + const account = await this.storage.get(ACCOUNT_TYPE, id); + if (account && account[LOGIN_COUNT] === 0) { + this.safeLogger.debug(`Removing account with no login methods ${id}`); + await this.storage.delete(ACCOUNT_TYPE, id); + } + } catch (error: unknown) { + // Prevent an unhandled rejection (e.g. a lock timeout) from crashing the process. + this.safeLogger.error(`Error during account cleanup of ${id}: ${createErrorMessage(error)}`); + } + }, this.timeout); + timer.unref(); + } +} diff --git a/src/index.ts b/src/index.ts index 2d9b1a8..a36ae93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ export * from "./storage/RdfPatchingStore"; export * from "./storage/patch/ThrowingN3Patcher"; export * from './FedcmHttpHandler'; export * from './http/output/PivotResponseWriter'; +export * from './identity/interaction/account/util/SafeBaseLoginAccountStorage'; export * from './identity/interaction/password/MigratedPasswordLoginHandler'; export * from './identity/PivotOidcHttpHandler';