feat: add lighthouse and fifth-domain entry flow
This commit is contained in:
parent
674c9e7764
commit
a35d61e227
12 changed files with 1056 additions and 3 deletions
|
|
@ -0,0 +1,159 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
INITIAL_DOMAIN_ACCESS_STATE,
|
||||
acceptDomainConnectionReceipt,
|
||||
acceptDomainSession,
|
||||
assertVerifiedDomainManifest,
|
||||
beginDomainAccess,
|
||||
disconnectDomainRuntime,
|
||||
enterDomainRuntime,
|
||||
projectPublicDomainVestibule,
|
||||
selectPublicDomain,
|
||||
type DomainConnectionReceipt,
|
||||
type DomainSessionCapability,
|
||||
type VerifiedDomainManifest,
|
||||
} from './domainRuntimeContract'
|
||||
|
||||
const DIGEST = 'a'.repeat(64)
|
||||
const NOW = 1_786_291_200_000
|
||||
|
||||
function manifest(): VerifiedDomainManifest {
|
||||
return {
|
||||
public: {
|
||||
accessModes: ['LOCAL_TERMINAL_NODE', 'CLOUD_RESIDENT_NODE'],
|
||||
displayName: '第五域 · 光湖本源域',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
formalName: '光湖本源域',
|
||||
publicStatus: 'PUBLIC_PREVIEW',
|
||||
purpose: '零点原核的工程本体与语言架构源点域',
|
||||
responsibleParty: '第五域主控',
|
||||
themePreview: {
|
||||
assetRef: 'theme-preview://origin-domain/lake-reflects-stars',
|
||||
description: '湖面映星公开预览',
|
||||
},
|
||||
},
|
||||
provenance: {
|
||||
digest: DIGEST,
|
||||
repositoryId: 'REPO-012',
|
||||
signature: 'signature-envelope-reference',
|
||||
signerId: 'SIGNER-FIFTH-DOMAIN-001',
|
||||
sourceCommit: '6e26d1f35439759542efb317c3ddd7661ba27d58',
|
||||
},
|
||||
runtime: {
|
||||
backendVersion: 'not-published',
|
||||
channelIds: ['CHANNEL-ZERO-CORE'],
|
||||
forbiddenDataScopes: ['private-relationship-core'],
|
||||
frontendVersion: 'not-published',
|
||||
healthCheckRef: 'health://origin-domain/runtime',
|
||||
moduleIds: ['MODULE-LANGUAGE-ARCHITECTURE'],
|
||||
rollbackVersion: 'not-published',
|
||||
routeRef: 'domain-route://origin-domain/runtime',
|
||||
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
|
||||
},
|
||||
schema: 'gh-aios.domain-manifest/v1',
|
||||
verification: {
|
||||
manifestDigest: DIGEST,
|
||||
state: 'verified',
|
||||
verifiedAt: NOW,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function session(overrides: Partial<DomainSessionCapability> = {}): DomainSessionCapability {
|
||||
return {
|
||||
capabilityId: 'CAP-001',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
expiresAt: NOW + 60_000,
|
||||
nodeId: 'LOCAL-NODE-001',
|
||||
scopes: ['domain:enter'],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function receipt(overrides: Partial<DomainConnectionReceipt> = {}): DomainConnectionReceipt {
|
||||
return {
|
||||
connectionId: 'CONNECTION-001',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
manifestDigest: DIGEST,
|
||||
nodeId: 'LOCAL-NODE-001',
|
||||
receiptId: 'RECEIPT-001',
|
||||
state: 'online',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('domain runtime contract', () => {
|
||||
it('projects only public vestibule fields', () => {
|
||||
const publicView = projectPublicDomainVestibule(manifest())
|
||||
const serialized = JSON.stringify(publicView)
|
||||
|
||||
expect(publicView.displayName).toBe('第五域 · 光湖本源域')
|
||||
expect(serialized).not.toContain('domain-route://')
|
||||
expect(serialized).not.toContain('CHANNEL-ZERO-CORE')
|
||||
expect(serialized).not.toContain('private-relationship-core')
|
||||
expect(serialized).not.toContain('REPO-012')
|
||||
})
|
||||
|
||||
it('rejects a manifest that has not passed the external signature verifier', () => {
|
||||
const candidate = manifest()
|
||||
candidate.verification.state = 'verified'
|
||||
candidate.verification.manifestDigest = 'b'.repeat(64)
|
||||
|
||||
expect(() => assertVerifiedDomainManifest(candidate)).toThrow('domain_manifest_digest_invalid')
|
||||
})
|
||||
|
||||
it.each(['LOCAL_TERMINAL_NODE', 'CLOUD_RESIDENT_NODE'] as const)(
|
||||
'accepts %s as a user-owned access node type',
|
||||
(nodeType) => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), nodeType)
|
||||
|
||||
expect(access).toMatchObject({ nodeType, stage: 'access' })
|
||||
},
|
||||
)
|
||||
|
||||
it('does not enter a runtime with only a selected vestibule or frontend login state', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
|
||||
expect(() => enterDomainRuntime(access, NOW)).toThrow('domain_runtime_session_missing')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
expect(() => enterDomainRuntime(withSession, NOW)).toThrow('domain_runtime_connection_receipt_missing')
|
||||
})
|
||||
|
||||
it('enters only after a matching live session and connection receipt', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
const connected = acceptDomainConnectionReceipt(withSession, receipt())
|
||||
const runtime = enterDomainRuntime(connected, NOW)
|
||||
|
||||
expect(runtime).toMatchObject({
|
||||
mode: 'connected',
|
||||
nodeType: 'LOCAL_TERMINAL_NODE',
|
||||
stage: 'runtime',
|
||||
})
|
||||
})
|
||||
|
||||
it('fails closed when the session or receipt belongs to another domain', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
|
||||
expect(() => acceptDomainSession(access, session({ domainId: 'DOMAIN-OTHER' }), NOW))
|
||||
.toThrow('domain_session_domain_mismatch')
|
||||
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
expect(() => acceptDomainConnectionReceipt(withSession, receipt({ domainId: 'DOMAIN-OTHER' })))
|
||||
.toThrow('domain_connection_domain_mismatch')
|
||||
})
|
||||
|
||||
it('drops execution authority and keeps an explicit read-only scene after disconnect', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
const connected = acceptDomainConnectionReceipt(withSession, receipt())
|
||||
const runtime = enterDomainRuntime(connected, NOW)
|
||||
|
||||
expect(disconnectDomainRuntime(runtime).mode).toBe('disconnected-readonly')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue