Files
makelore/electron/coding-plugins/hosted-admission.ts

185 lines
6.7 KiB
TypeScript

import type { MarketplaceResolveItem } from './account-plugin-cache';
import type { MarketplacePackageClientPort, PluginPackageStore } from './package-store';
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const REQUEST_ID_PATTERN = /^[\x21-\x7e]{1,128}$/u;
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
export interface HostedAdmissionWorkerSnapshot {
/** Main-issued identity for the logical worker operation. */
readonly requestId: string;
/** Release frozen into this parent worker's effective snapshot. */
readonly pluginReleaseId?: string;
}
export interface MarketplaceHostedAdmissionInput {
readonly pluginId: string;
readonly workerSnapshot: HostedAdmissionWorkerSnapshot;
}
export interface MarketplaceHostedAdmission {
readonly releaseId: string;
readonly releaseAdmissionId: string;
}
export type MarketplaceHostedAdmissionErrorCode =
| 'plugin_release_unavailable'
| 'plugin_runtime_stale'
| 'plugin_account_changed'
| 'plugin_backend_unavailable';
export class MarketplaceHostedAdmissionError extends Error {
constructor(
readonly code: MarketplaceHostedAdmissionErrorCode,
readonly status: number,
readonly retryable: boolean,
message: string,
) {
super(message);
this.name = 'MarketplaceHostedAdmissionError';
}
}
export interface MarketplaceHostedAdmissionResolverOptions {
readonly packageStore: Pick<PluginPackageStore, 'getInstalledRelease'>;
readonly marketplace: Pick<MarketplacePackageClientPort, 'resolve'>;
readonly makeloreVersion: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function errorCode(value: unknown): unknown {
return isRecord(value) ? value.code : undefined;
}
function releaseId(value: unknown): string | null {
return typeof value === 'string' && RELEASE_ID_PATTERN.test(value) ? value : null;
}
function pluginId(value: unknown): string | null {
return typeof value === 'string' && PLUGIN_ID_PATTERN.test(value) ? value : null;
}
function requestId(value: unknown): string | null {
return typeof value === 'string' && REQUEST_ID_PATTERN.test(value) ? value : null;
}
function stale(message: string): MarketplaceHostedAdmissionError {
return new MarketplaceHostedAdmissionError('plugin_runtime_stale', 409, false, message);
}
function unavailable(message: string): MarketplaceHostedAdmissionError {
return new MarketplaceHostedAdmissionError('plugin_release_unavailable', 409, false, message);
}
function backendUnavailable(): MarketplaceHostedAdmissionError {
return new MarketplaceHostedAdmissionError(
'plugin_backend_unavailable',
503,
true,
'Hosted Plugin admission service is temporarily unavailable',
);
}
function accountChanged(): MarketplaceHostedAdmissionError {
return new MarketplaceHostedAdmissionError(
'plugin_account_changed',
409,
false,
'Marketplace account changed while resolving hosted Plugin admission',
);
}
function matchesFrozenRelease(
item: MarketplaceResolveItem,
installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>>,
plugin: string,
): item is MarketplaceResolveItem & {
readonly releaseId: string;
readonly releaseAdmissionId: string;
} {
if (!installed) return false;
if (item.pluginId !== plugin || (item.action !== 'keep' && item.action !== 'install')) return false;
if (releaseId(item.releaseId) !== installed.releaseId
|| releaseId(item.releaseAdmissionId) === null
|| item.releaseAdmissionId === undefined
|| item.releaseAdmissionId === null) return false;
if (item.version !== installed.version || item.sha256 !== installed.sha256) return false;
if (item.sizeBytes !== installed.sizeBytes || item.channel !== installed.channel) return false;
return item.sha256 !== null && item.sha256 !== undefined && SHA256_PATTERN.test(item.sha256);
}
/**
* Resolves the current account admission for the exact Release frozen into a
* parent worker. Marketplace identity and Release freshness are the only
* concerns here; provider, payload, billing, and transport routes remain
* outside this boundary.
*/
export class MarketplaceHostedAdmissionResolver {
constructor(private readonly options: MarketplaceHostedAdmissionResolverOptions) {}
async resolve(input: MarketplaceHostedAdmissionInput): Promise<MarketplaceHostedAdmission> {
const plugin = pluginId(input?.pluginId);
const worker = input?.workerSnapshot;
const request = requestId(worker?.requestId);
const frozenRelease = releaseId(worker?.pluginReleaseId);
if (!plugin || !request) throw unavailable('Hosted Plugin worker identity is unavailable');
if (!frozenRelease) throw unavailable('Hosted Plugin worker Release is unavailable');
let installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>>;
try {
installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease);
} catch (error) {
throw this.mapError(error);
}
if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease
|| installed.unavailableReason || !installed.channel) {
throw unavailable('Installed hosted Plugin Release is unavailable');
}
let resolved;
try {
resolved = await this.options.marketplace.resolve({
resolveRequestId: request,
makeloreVersion: this.options.makeloreVersion,
channel: installed.channel,
installed: [{
pluginId: installed.pluginId,
releaseId: installed.releaseId,
sha256: installed.sha256,
}],
});
} catch (error) {
throw this.mapError(error);
}
if (resolved.stale || resolved.resolveRequestId !== request) {
throw stale('Hosted Plugin worker admission is stale');
}
const item = resolved.items.find(({ pluginId: candidate }) => candidate === plugin);
if (!item || !matchesFrozenRelease(item, installed, plugin)) {
throw stale('Hosted Plugin worker Release admission is stale');
}
return Object.freeze({
releaseId: item.releaseId,
releaseAdmissionId: item.releaseAdmissionId,
});
}
private mapError(error: unknown): MarketplaceHostedAdmissionError {
if (error instanceof MarketplaceHostedAdmissionError) return error;
const code = errorCode(error);
if (code === 'marketplace_account_changed' || code === 'plugin_account_changed') return accountChanged();
return backendUnavailable();
}
}
export function createMarketplaceHostedAdmissionResolver(
options: MarketplaceHostedAdmissionResolverOptions,
): MarketplaceHostedAdmissionResolver {
return new MarketplaceHostedAdmissionResolver(options);
}