178 lines
5.7 KiB
TypeScript
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),
|
||
|
|
};
|
||
|
|
}
|