20 lines
6 KiB
JavaScript
20 lines
6 KiB
JavaScript
export const operatorRequestType="voidbot.discord.epiphany_operator_request";
|
|
export const operatorRequestSchema="voidbot.discord.epiphany_operator_request.v0";
|
|
export const operatorAdmissionType="bifrost.operator_command.delivery";
|
|
export const operatorAdmissionSchema="bifrost.operator_command.delivery.v0";
|
|
export const operatorResultType="epiphany.operator_command.result";
|
|
export const operatorResultSchema="epiphany.operator_command.result.v0";
|
|
|
|
export function buildOperatorDefinitions(defineDocumentType){const make=(type,schemaId,name,parse)=>defineDocumentType({type,schemaName:type,schemaId,schemaVersion:schemaId,contentHash:schemaId,global:false,name,schema:{parse}});return {request:make(operatorRequestType,operatorRequestSchema,"requestId",parseOperatorRequest),admission:make(operatorAdmissionType,operatorAdmissionSchema,"admissionId",parseOperatorAdmission),result:make(operatorResultType,operatorResultSchema,"resultId",parseOperatorResult)};}
|
|
export function parseOperatorRequest(value){const v=exactRecord(value,["schemaName","schemaVersion","requestId","commandId","nonce","sourceEventId","sourceActorDiscordId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","issuedAt","expiresAt","producerId","producerRuntimeId","authorityClass","status","command"],"operator request"); exact(v.schemaName,operatorRequestType,"schemaName");exact(v.schemaVersion,operatorRequestSchema,"schemaVersion"); for(const k of ["requestId","commandId","nonce","sourceEventId","sourceActorDiscordId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","producerRuntimeId"])text(v[k],k); exact(v.producerId,"voidbot","producerId");exact(v.authorityClass,"operator_request_only","authorityClass");exact(v.status,"pending","status"); const issued=date(v.issuedAt,"issuedAt"),expires=date(v.expiresAt,"expiresAt");if(expires<=issued||expires-issued>60000)throw new Error("operator request TTL must be positive and at most 60 seconds.");parseCommand(v.command);return v;}
|
|
export function parseOperatorAdmission(v){v=exactRecord(v,["schemaName","schemaVersion","admissionId","packet","packetSha256","sourceObserverId","sourceObserverRuntimeId","provider","bifrostAdmissionReceiptId","authority","providerIdentityId","providerSignature"],"operator admission");exact(v.schemaName,operatorAdmissionType,"schemaName");exact(v.schemaVersion,operatorAdmissionSchema,"schemaVersion");for(const k of ["admissionId","packetSha256","sourceObserverRuntimeId","bifrostAdmissionReceiptId","providerIdentityId"])text(v[k],k);exact(v.sourceObserverId,"voidbot","sourceObserverId");exact(v.provider,"bifrost","provider");exact(v.authority,"exact_operator_command_only","authority");if(!(v.providerSignature instanceof Uint8Array)||v.providerSignature.length!==64)throw new Error("providerSignature must be 64 bytes.");parsePacket(v.packet);return v;}
|
|
export function parseOperatorResult(v){v=exactRecord(v,["schemaVersion","resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","consequenceRef","completedAt","privateStateExposed","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"],"operator result");exact(v.schemaVersion,operatorResultSchema,"schemaVersion");for(const k of ["resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","completedAt"])text(v[k],k);if(!["observed","applied","refused"].includes(v.disposition))throw new Error("result disposition is invalid.");if(v.privateStateExposed!==false)throw new Error("operator result must not expose private state.");for(const k of ["consequenceRef","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"])if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);date(v.completedAt,"completedAt");return v;}
|
|
export function packetTuple(p){return [p.commandId,p.nonce,p.sourceEventId,p.sourceActorId,p.discordGuildId,p.discordChannelId,p.discordMessageId,p.targetRuntimeId,p.issuedAt,p.expiresAt,commandTuple(p.command)];}
|
|
export function admissionTuple(a){return [a.schemaVersion,a.admissionId,packetTuple(a.packet),a.packetSha256,a.sourceObserverId,a.sourceObserverRuntimeId,a.provider,a.bifrostAdmissionReceiptId,a.authority,a.providerIdentityId];}
|
|
function parsePacket(v){v=exactRecord(v,["commandId","nonce","sourceEventId","sourceActorId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","issuedAt","expiresAt","command"],"operator packet");for(const k of ["commandId","nonce","sourceEventId","sourceActorId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId"])text(v[k],k);date(v.issuedAt,"issuedAt");date(v.expiresAt,"expiresAt");parseCommand(v.command);return v;}
|
|
function parseCommand(v){if(!v||typeof v!=="object"||Array.isArray(v))throw new Error("command must be an object.");if(v.kind==="status"||v.kind==="wake")return exactRecord(v,["kind"],"command");if(v.kind==="sleep"){exactRecord(v,["kind","reason"],"command");text(v.reason,"reason");if(v.reason.length>512)throw new Error("sleep reason exceeds 512 characters.");return v;}if(v.kind==="directive"){exactRecord(v,["kind","objective"],"command");text(v.objective,"objective");if(v.objective.length>4096)throw new Error("directive objective exceeds 4096 characters.");return v;}throw new Error("command kind is invalid.");}
|
|
function commandTuple(v){if(v.kind==="sleep")return {kind:"sleep",reason:v.reason};if(v.kind==="directive")return {kind:"directive",objective:v.objective};return {kind:v.kind};}
|
|
function exactRecord(v,keys,label){if(!v||typeof v!=="object"||Array.isArray(v))throw new Error(`${label} must be an object.`);const actual=Object.keys(v).sort(),expected=[...keys].sort();if(actual.join("\0")!==expected.join("\0"))throw new Error(`${label} has unknown or missing fields.`);return v;}
|
|
function text(v,label){if(typeof v!=="string"||!v.trim())throw new Error(`${label} is required.`);return v;}
|
|
function exact(v,e,label){if(v!==e)throw new Error(`${label} must be ${e}.`);}
|
|
function date(v,label){text(v,label);const n=Date.parse(v);if(!Number.isFinite(n)||!/T/.test(v))throw new Error(`${label} must be RFC3339.`);return n;}
|