ltijs
Version:
Easily turn your web application into a LTI 1.3 Learning Tool.
152 lines • 8.2 kB
JavaScript
;
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