2026-06-08 06:41:34 +00:00

178 lines
5.7 KiB
TypeScript

export interface HiveCastProviderConfig {
readonly cloud: string;
readonly apiKey: string;
readonly space: string;
readonly profile?: string;
readonly fetch?: typeof fetch;
}
export interface HiveCastActorCredentialRequest {
readonly mount: string;
readonly actorId?: string;
readonly packageId?: string;
readonly runtimeId?: string;
readonly accepts?: Record<string, unknown>;
readonly emits?: Record<string, unknown>;
readonly subscribes?: Record<string, unknown>;
readonly ttlSeconds?: number;
}
export interface HiveCastCredentialPair {
readonly jwt: string;
readonly seed: string;
readonly userPublicKey?: string;
readonly expiresAt?: string;
}
export interface HiveCastTransportInfo {
readonly root: string;
readonly natsUrl?: string;
readonly natsWsUrl?: string;
readonly leafnodeUrl?: string;
}
export interface HiveCastActorCredential {
readonly ok: true;
readonly cloud: string;
readonly space: string;
readonly root: string;
readonly mount: string;
readonly effectiveMount?: string;
readonly transport: HiveCastTransportInfo;
readonly credentials: HiveCastCredentialPair;
readonly key?: Record<string, unknown>;
readonly source?: string;
readonly credentialPipeline?: string;
}
export interface HiveCastKeyInfo {
readonly ok: true;
readonly cloud: string;
readonly space: string;
readonly key?: Record<string, unknown>;
readonly principal?: Record<string, unknown>;
readonly profile?: Record<string, unknown>;
}
function requiredString(value: unknown, label: string): string {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`HiveCast response missing ${label}`);
}
return value.trim();
}
function optionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function record(value: unknown): Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function normalizeCloud(cloud: string): string {
const normalized = cloud.trim().replace(/\/+$/g, '');
if (!normalized) throw new Error('HiveCast cloud URL is required');
return normalized;
}
async function postHiveCastJson(
config: HiveCastProviderConfig,
path: string,
body: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const cloud = normalizeCloud(config.cloud);
const fetchImpl = config.fetch ?? globalThis.fetch;
if (typeof fetchImpl !== 'function') {
throw new Error('HiveCast provider requires fetch');
}
const response = await fetchImpl(`${cloud}${path}`, {
method: 'POST',
headers: {
authorization: `Bearer ${config.apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
const text = await response.text();
const parsed = text.trim() ? record(JSON.parse(text)) : {};
if (!response.ok || parsed.ok !== true) {
const error = optionalString(parsed.error) ?? `HTTP_${response.status}`;
throw new Error(`HiveCast ${path} failed: ${error}`);
}
return parsed;
}
export async function validateHiveCastApiKey(config: HiveCastProviderConfig): Promise<HiveCastKeyInfo> {
const cloud = normalizeCloud(config.cloud);
const body = await postHiveCastJson(config, '/api/install/key-info', {
space: config.space,
...(config.profile ? { profile: config.profile } : {}),
});
return {
ok: true,
cloud,
space: config.space,
key: record(body.key),
principal: record(body.principal),
profile: record(body.profile),
};
}
export async function exchangeHiveCastActorCredential(
config: HiveCastProviderConfig,
request: HiveCastActorCredentialRequest,
): Promise<HiveCastActorCredential> {
const cloud = normalizeCloud(config.cloud);
if (!config.apiKey.trim()) throw new Error('HiveCast API key is required');
if (!config.space.trim()) throw new Error('HiveCast space is required');
if (!request.mount.trim()) throw new Error('HiveCast actor credential exchange requires mount');
const body = await postHiveCastJson(config, '/api/install/actor-credential', {
space: config.space,
mount: request.mount,
...(config.profile ? { profile: config.profile } : {}),
...(request.actorId ? { actorId: request.actorId } : {}),
...(request.packageId ? { packageId: request.packageId } : {}),
...(request.runtimeId ? { runtimeId: request.runtimeId } : {}),
...(request.accepts ? { accepts: request.accepts } : {}),
...(request.emits ? { emits: request.emits } : {}),
...(request.subscribes ? { subscribes: request.subscribes } : {}),
...(request.ttlSeconds ? { ttlSeconds: request.ttlSeconds } : {}),
});
const transport = record(body.transport);
const credentials = record(body.credentials);
const space = record(body.space);
const root = optionalString(transport.root)
?? optionalString(space.authorityRoot)
?? optionalString(body.authorityRoot)
?? optionalString(body.root)
?? config.space;
return {
ok: true,
cloud,
space: config.space,
root,
mount: request.mount,
effectiveMount: optionalString(body.effectiveMount),
transport: {
root,
natsUrl: optionalString(transport.natsUrl),
natsWsUrl: optionalString(transport.natsWsUrl),
leafnodeUrl: optionalString(transport.leafnodeUrl),
},
credentials: {
jwt: requiredString(credentials.jwt, 'credentials.jwt'),
seed: requiredString(credentials.seed, 'credentials.seed'),
userPublicKey: optionalString(credentials.userPublicKey),
expiresAt: optionalString(credentials.expiresAt),
},
key: record(body.key),
source: optionalString(body.source),
credentialPipeline: optionalString(body.credentialPipeline),
};
}