hololake-system-architecture/product-source/hololake-platform/src/lib/domainRuntimeContract.ts

269 lines
8.5 KiB
TypeScript
Raw Normal View History

export type DomainNodeType = 'LOCAL_TERMINAL_NODE' | 'CLOUD_RESIDENT_NODE'
export type PublicDomainStatus = 'PUBLIC_PREVIEW' | 'RESTRICTED_PREVIEW' | 'UNAVAILABLE'
export interface PublicThemePreview {
assetRef: string
description: string
}
export interface PublicDomainVestibule {
accessModes: DomainNodeType[]
displayName: string
domainId: string
formalName: string
publicStatus: PublicDomainStatus
purpose: string
responsibleParty: string
themePreview: PublicThemePreview | null
}
export interface DomainManifestProvenance {
digest: string
repositoryId: string
signature: string
signerId: string
sourceCommit: string
}
export interface DomainRuntimeDescriptor {
backendVersion: string
channelIds: string[]
forbiddenDataScopes: string[]
frontendVersion: string
healthCheckRef: string
moduleIds: string[]
rollbackVersion: string
routeRef: string
themePackageRef: string
}
export interface VerifiedDomainManifest {
public: PublicDomainVestibule
provenance: DomainManifestProvenance
runtime: DomainRuntimeDescriptor
schema: 'gh-aios.domain-manifest/v1'
verification: {
manifestDigest: string
state: 'verified'
verifiedAt: number
}
}
export interface DomainSessionCapability {
capabilityId: string
domainId: string
expiresAt: number
nodeId: string
scopes: string[]
}
export interface DomainConnectionReceipt {
connectionId: string
domainId: string
manifestDigest: string
nodeId: string
receiptId: string
state: 'online'
}
export interface LighthouseState {
stage: 'lighthouse'
}
export interface VestibuleState {
domain: PublicDomainVestibule
stage: 'vestibule'
}
export interface AccessState {
manifest: VerifiedDomainManifest
nodeType: DomainNodeType
receipt: DomainConnectionReceipt | null
session: DomainSessionCapability | null
stage: 'access'
}
export interface RuntimeState {
manifest: VerifiedDomainManifest
mode: 'connected' | 'disconnected-readonly'
nodeType: DomainNodeType
receipt: DomainConnectionReceipt
session: DomainSessionCapability
stage: 'runtime'
}
export type DomainAccessState = LighthouseState | VestibuleState | AccessState | RuntimeState
export class DomainContractError extends Error {
readonly code: string
constructor(code: string) {
super(code)
this.code = code
this.name = 'DomainContractError'
}
}
const SHA256 = /^[a-f0-9]{64}$/
const SOURCE_COMMIT = /^[a-f0-9]{40,64}$/
const REQUIRED_ENTER_SCOPE = 'domain:enter'
function required(value: string, code: string): string {
if (!value.trim()) throw new DomainContractError(code)
return value
}
function publicDomain(domain: PublicDomainVestibule): PublicDomainVestibule {
required(domain.domainId, 'domain_manifest_public_id_missing')
required(domain.formalName, 'domain_manifest_formal_name_missing')
required(domain.displayName, 'domain_manifest_display_name_missing')
required(domain.purpose, 'domain_manifest_public_purpose_missing')
required(domain.responsibleParty, 'domain_manifest_responsible_party_missing')
if (!domain.accessModes.length) {
throw new DomainContractError('domain_manifest_access_modes_missing')
}
if (domain.accessModes.some(mode => mode !== 'LOCAL_TERMINAL_NODE' && mode !== 'CLOUD_RESIDENT_NODE')) {
throw new DomainContractError('domain_manifest_access_mode_invalid')
}
if (domain.themePreview) {
required(domain.themePreview.assetRef, 'domain_manifest_theme_preview_ref_missing')
required(domain.themePreview.description, 'domain_manifest_theme_preview_description_missing')
}
return {
accessModes: [...domain.accessModes],
displayName: domain.displayName,
domainId: domain.domainId,
formalName: domain.formalName,
publicStatus: domain.publicStatus,
purpose: domain.purpose,
responsibleParty: domain.responsibleParty,
themePreview: domain.themePreview ? { ...domain.themePreview } : null,
}
}
export function assertVerifiedDomainManifest(
manifest: VerifiedDomainManifest,
): VerifiedDomainManifest {
if (manifest.schema !== 'gh-aios.domain-manifest/v1') {
throw new DomainContractError('domain_manifest_schema_unsupported')
}
publicDomain(manifest.public)
if (manifest.verification.state !== 'verified' || !Number.isFinite(manifest.verification.verifiedAt)) {
throw new DomainContractError('domain_manifest_not_verified')
}
if (!SHA256.test(manifest.provenance.digest)
|| manifest.verification.manifestDigest !== manifest.provenance.digest) {
throw new DomainContractError('domain_manifest_digest_invalid')
}
if (!SOURCE_COMMIT.test(manifest.provenance.sourceCommit)) {
throw new DomainContractError('domain_manifest_source_commit_invalid')
}
required(manifest.provenance.repositoryId, 'domain_manifest_repository_missing')
required(manifest.provenance.signerId, 'domain_manifest_signer_missing')
required(manifest.provenance.signature, 'domain_manifest_signature_missing')
required(manifest.runtime.routeRef, 'domain_manifest_route_missing')
required(manifest.runtime.themePackageRef, 'domain_manifest_theme_package_missing')
required(manifest.runtime.healthCheckRef, 'domain_manifest_health_check_missing')
required(manifest.runtime.rollbackVersion, 'domain_manifest_rollback_version_missing')
return manifest
}
export function projectPublicDomainVestibule(
manifest: VerifiedDomainManifest,
): PublicDomainVestibule {
return publicDomain(assertVerifiedDomainManifest(manifest).public)
}
export const INITIAL_DOMAIN_ACCESS_STATE: LighthouseState = { stage: 'lighthouse' }
export function selectPublicDomain(
state: LighthouseState | VestibuleState,
domain: PublicDomainVestibule,
): VestibuleState {
void state
return { domain: publicDomain(domain), stage: 'vestibule' }
}
export function beginDomainAccess(
state: VestibuleState,
manifest: VerifiedDomainManifest,
nodeType: DomainNodeType,
): AccessState {
const verified = assertVerifiedDomainManifest(manifest)
if (verified.public.domainId !== state.domain.domainId) {
throw new DomainContractError('domain_manifest_selected_domain_mismatch')
}
if (!verified.public.accessModes.includes(nodeType)) {
throw new DomainContractError('domain_node_type_not_allowed')
}
return {
manifest: verified,
nodeType,
receipt: null,
session: null,
stage: 'access',
}
}
export function acceptDomainSession(
state: AccessState,
session: DomainSessionCapability,
now: number,
): AccessState {
if (session.domainId !== state.manifest.public.domainId) {
throw new DomainContractError('domain_session_domain_mismatch')
}
if (session.expiresAt <= now) {
throw new DomainContractError('domain_session_expired')
}
if (!session.scopes.includes(REQUIRED_ENTER_SCOPE)) {
throw new DomainContractError('domain_session_enter_scope_missing')
}
required(session.capabilityId, 'domain_session_capability_id_missing')
required(session.nodeId, 'domain_session_node_id_missing')
return { ...state, session: { ...session, scopes: [...session.scopes] } }
}
export function acceptDomainConnectionReceipt(
state: AccessState,
receipt: DomainConnectionReceipt,
): AccessState {
if (!state.session) {
throw new DomainContractError('domain_connection_session_missing')
}
if (receipt.domainId !== state.manifest.public.domainId) {
throw new DomainContractError('domain_connection_domain_mismatch')
}
if (receipt.nodeId !== state.session.nodeId) {
throw new DomainContractError('domain_connection_node_mismatch')
}
if (receipt.manifestDigest !== state.manifest.provenance.digest) {
throw new DomainContractError('domain_connection_manifest_mismatch')
}
required(receipt.connectionId, 'domain_connection_id_missing')
required(receipt.receiptId, 'domain_connection_receipt_id_missing')
return { ...state, receipt: { ...receipt } }
}
export function enterDomainRuntime(state: AccessState, now: number): RuntimeState {
if (!state.session) throw new DomainContractError('domain_runtime_session_missing')
if (!state.receipt) throw new DomainContractError('domain_runtime_connection_receipt_missing')
if (state.session.expiresAt <= now) throw new DomainContractError('domain_session_expired')
return {
manifest: state.manifest,
mode: 'connected',
nodeType: state.nodeType,
receipt: state.receipt,
session: state.session,
stage: 'runtime',
}
}
export function disconnectDomainRuntime(state: RuntimeState): RuntimeState {
return { ...state, mode: 'disconnected-readonly' }
}
export function returnToLighthouse(): LighthouseState {
return INITIAL_DOMAIN_ACCESS_STATE
}