UNPKG

ltijs

Version:

Easily turn your web application into a LTI 1.3 Learning Tool.

152 lines • 8.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlatformManager = void 0; const validation_1 = require("#utils/validation/validation"); const keys_1 = require("#utils/crypto/keys"); const platform_manager_schemas_1 = require("#services/platform-manager/platform-manager.schemas"); const platform_serializer_1 = require("#services/platform-manager/platform.serializer"); const platform_keys_1 = require("#services/platform-manager/platform-keys"); const errors_1 = require("#services/platform-manager/errors"); const keyset_constants_1 = require("#services/keyset/keyset.constants"); /** Registers, looks up, updates, and (de)activates platforms. Reachable as `Provider.platformManager`. */ class PlatformManager { LOG_COMPONENT = 'platformManager'; databaseManager; logger; cacheManager; constructor(databaseManager, logger, cacheManager) { this.databaseManager = databaseManager; this.logger = logger; this.cacheManager = cacheManager; } /** Registers a new platform, generating its RSA keypair. Throws `PlatformAlreadyRegisteredError` if the `url`/`clientId` pair already exists. */ async registerPlatform(platform) { const validated = (0, validation_1.validate)(platform_manager_schemas_1.PlatformRegistrationInputSchema, platform); const existingPlatform = await this.getPlatformByUrlAndClientId(validated.url, validated.clientId); if (existingPlatform !== undefined) throw new errors_1.PlatformAlreadyRegisteredError(); return await this.createNewPlatform(validated); } /** Lists registered platforms, optionally narrowed by `filter` (e.g. `{ active: true }`). */ async getPlatforms(filter) { const validated = filter !== undefined ? (0, validation_1.validate)(platform_manager_schemas_1.PlatformSearchInputSchema, filter) : undefined; const records = await this.databaseManager.getPlatforms(validated); return records.map(record => (0, platform_serializer_1.buildPlatform)(record)); } async getPlatformById(platformId) { const record = await this.databaseManager.getPlatformById(platformId); if (record !== undefined) return (0, platform_serializer_1.buildPlatform)(record); } /** The lookup used at login time: `url`/`clientId` together identify a platform, mirroring how the OIDC login request identifies it. */ async getPlatformByUrlAndClientId(url, clientId) { const record = await this.databaseManager.getPlatformByUrlAndClientId(url, clientId); if (record !== undefined) return (0, platform_serializer_1.buildPlatform)(record); } /** * Writes only the fields present in `update` (never the rest of `platform`'s current fields), so a * concurrent update to a different field can't be silently reverted. Throws * `UrlClientIdCombinationAlreadyExistsError` if changing `url`/`clientId` would collide with another * platform. */ async updatePlatform(platform, update) { const validated = (0, validation_1.validate)(platform_manager_schemas_1.PlatformUpdateInputSchema, update); const changes = this.buildPlatformUpdate(platform, validated); const newUrl = changes.url ?? platform.url; const newClientId = changes.clientId ?? platform.clientId; if (newUrl !== platform.url || newClientId !== platform.clientId) { const collision = await this.databaseManager.getPlatformByUrlAndClientId(newUrl, newClientId); if (collision !== undefined) throw new errors_1.UrlClientIdCombinationAlreadyExistsError(); } await this.databaseManager.updatePlatformById(platform.id, changes); this.logger.debug(this.LOG_COMPONENT, `Platform updated: ${platform.id}`); return (0, platform_serializer_1.buildPlatform)({ ...platform, ...changes }); } async deletePlatform(platform) { await this.databaseManager.deletePlatformById(platform.id); await this.invalidateKeysetCache(); this.logger.debug(this.LOG_COMPONENT, `Platform deleted: ${platform.id}`); } /** A deactivated platform's login/launch requests are rejected (or routed to `onInactivePlatform`, if set) until reactivated. */ async activatePlatform(platform) { await this.databaseManager.updatePlatformById(platform.id, { active: true }); this.logger.debug(this.LOG_COMPONENT, `Platform activated: ${platform.id}`); return (0, platform_serializer_1.buildPlatform)({ ...platform, active: true }); } async deactivatePlatform(platform) { await this.databaseManager.updatePlatformById(platform.id, { active: false }); this.logger.debug(this.LOG_COMPONENT, `Platform deactivated: ${platform.id}`); return (0, platform_serializer_1.buildPlatform)({ ...platform, active: false }); } /** Generates a fresh RSA keypair for the platform and invalidates the cached keyset: the old key stops being served immediately. */ async rotateKeys(platform) { const keys = await (0, keys_1.generateKeyPair)(); await this.databaseManager.updatePlatformById(platform.id, { keys }); await this.invalidateKeysetCache(); this.logger.debug(this.LOG_COMPONENT, `Platform keys rotated: ${platform.id}`); return (0, platform_serializer_1.buildPlatform)({ ...platform, keys }); } async getPublicKey(platform) { return (0, platform_keys_1.resolvePlatformPublicKey)(platform); } async getPrivateKey(platform) { return (0, platform_keys_1.resolvePlatformPrivateKey)(platform); } async createNewPlatform(platform) { const keys = await (0, keys_1.generateKeyPair)(); const platformAttributes = { active: true, keys, ...platform, }; const id = await this.databaseManager.savePlatform(platformAttributes); await this.invalidateKeysetCache(); this.logger.debug(this.LOG_COMPONENT, `Platform registered: ${id}`); const record = { ...platformAttributes, id }; return (0, platform_serializer_1.buildPlatform)(record); } // Reaches directly into KeysetService's cache key instead of taking a KeysetService dependency, since // Provider constructs KeysetService *after* PlatformManager (which KeysetService itself depends on); // the reverse dependency would be circular. async invalidateKeysetCache() { await this.cacheManager.delete(keyset_constants_1.KEYSET_CACHE_KEY); } // `update` is already a true partial: Zod omits an absent optional field from its parsed output // entirely, rather than including it as `undefined`, so every field here already matches // `PlatformRecord`'s shape one-for-one. `idTokenValidation` is the one exception: `update`'s version // is itself partial (only one of `method`/`key` may be given), but a `$set` on a nested object // replaces it wholesale, so the other sub-field still needs filling in from `existing` to avoid // wiping it out. buildPlatformUpdate(existing, update) { const { idTokenValidation, ...rest } = update; if (idTokenValidation === undefined) return rest; return { ...rest, idTokenValidation: { method: idTokenValidation.method ?? existing.idTokenValidation.method, key: idTokenValidation.key ?? existing.idTokenValidation.key, }, }; } // Deprecated methods /** * @deprecated Kept as a v5-migration bridge. Use {@link PlatformManager.getPlatforms} with no parameters instead. */ async getAllPlatforms() { return await this.getPlatforms(); } async getPlatform(url, clientId) { const platforms = await this.getPlatforms({ url, clientId }); if (platforms.length === 0) return false; if (clientId !== undefined) return platforms[0]; return platforms; } } exports.PlatformManager = PlatformManager; exports.default = PlatformManager; //# sourceMappingURL=platform-manager.service.js.map