Separate domain access from code-channel login
This commit is contained in:
parent
066f14d007
commit
6b1bb203df
7 changed files with 324 additions and 6 deletions
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"schema": "guanghu.desktop-delivery-receipt/v1",
|
||||
"receipt_id": "GH-HOLOLAKE-DOMAIN-CONNECTION-SEPARATION-20260810-001",
|
||||
"development_id": "DEV-20260809-007",
|
||||
"source_base_commit": "066f14d0074bd6315fd21da4fbd39948559ea2c6",
|
||||
"source_commit": "SELF",
|
||||
"repository": "repo://guanghulab.com/code/bingshuo/hololake-system-architecture#main",
|
||||
"acceptance": {
|
||||
"public_entry_opens_domain_gate": true,
|
||||
"code_channel_account_is_not_domain_login": true,
|
||||
"runtime_entry_fails_closed_without_handoff": true,
|
||||
"local_workspace_remains_explicitly_separate": true,
|
||||
"local_and_cloud_node_modes_are_truthfully_projected": true
|
||||
},
|
||||
"implementation": {
|
||||
"domain_connection_sheet": "Projects account and registered-node identity, signed domain manifest, scoped session capability and matching online receipt as four separate gates.",
|
||||
"code_channel_boundary": "Forgejo account and Git remote settings remain available through a separately labelled code-channel and sync path.",
|
||||
"ui_layering": "Connection and storage dialogs render above the public lighthouse instead of existing only in the accessibility tree behind it.",
|
||||
"status_copy": "The workspace top bar now says code-channel account and no longer claims that Forgejo authentication is Fifth Domain login."
|
||||
},
|
||||
"verification": {
|
||||
"tests": "PASS 49; FAIL 0",
|
||||
"production_build": "PASS",
|
||||
"developer_id_deep_strict_codesign": "PASS",
|
||||
"installed_app_binary_sha256": "ed4dd0d0dad51a4aed5fac62263710ab502c744aad0271410d9c245118894b60",
|
||||
"installed_app_asar_sha256": "7dc31460ccfc11042171a03f0c773a0e27cb53c3267c7c9965e36c15943dfcce",
|
||||
"current_desktop_visual_acceptance": "PASS: public login visibly opened the domain gate; all four unverified gates were shown; runtime entry stayed disabled; code-channel settings opened separately with explicit non-domain-authority copy."
|
||||
},
|
||||
"not_claimed": [
|
||||
"No target-domain signer, live handoff endpoint, domain session capability or online runtime receipt exists yet.",
|
||||
"No server deployment, production cutover, real Fifth Domain runtime entry or persona-birth claim is made.",
|
||||
"No Apple notarization was performed; the installed development delivery is Developer ID signed."
|
||||
],
|
||||
"result": "PASS_100_LOCAL_DOMAIN_CONNECTION_BOUNDARY",
|
||||
"verified_at": "2026-08-10T04:37:44+08:00"
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { DomainSurface } from './components/DomainSurface';
|
|||
import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
|
||||
import { cleanDisplayText } from './presentation';
|
||||
import { WorldEntry } from './components/WorldEntry';
|
||||
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
|
||||
|
||||
type View = 'editor' | 'history';
|
||||
type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
|
||||
|
|
@ -80,6 +81,7 @@ export default function App() {
|
|||
const [activeRoute, setActiveRoute] = useState<RouteId>('fifth');
|
||||
const [activeModule, setActiveModule] = useState<ModuleId>('knowledge');
|
||||
const [storageSheetOpen, setStorageSheetOpen] = useState(false);
|
||||
const [domainConnectionOpen, setDomainConnectionOpen] = useState(false);
|
||||
const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>();
|
||||
const [serverSession, setServerSession] = useState<ServerSession>({ authenticated: false, nodeId: '' });
|
||||
const [serverProfiles, setServerProfiles] = useState<ServerProfile[]>([]);
|
||||
|
|
@ -391,8 +393,7 @@ export default function App() {
|
|||
setWorldEntered(true);
|
||||
}}
|
||||
onOpenConnection={() => {
|
||||
setStorageSheetInitialMode('server');
|
||||
setStorageSheetOpen(true);
|
||||
setDomainConnectionOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -414,7 +415,7 @@ export default function App() {
|
|||
setStorageSheetOpen(true);
|
||||
}}>
|
||||
<span className="session-status-dot" aria-hidden="true" />
|
||||
<span><strong>{serverSession.authenticated ? serverSession.username : (personalServer ? '登录第五域' : '配置我的服务器')}</strong><small>{personalServer?.id || '本机私有配置'}</small></span>
|
||||
<span><strong>{serverSession.authenticated ? `代码频道 · ${serverSession.username}` : (personalServer ? '代码频道账号' : '配置我的服务器')}</strong><small>{personalServer?.id || '本机私有配置'}</small></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
|
@ -577,6 +578,23 @@ export default function App() {
|
|||
refreshDomainAccess();
|
||||
}}
|
||||
/>
|
||||
<DomainConnectionSheet
|
||||
access={domainAccess}
|
||||
open={domainConnectionOpen}
|
||||
onClose={() => setDomainConnectionOpen(false)}
|
||||
onEnterRuntime={() => {
|
||||
if (!domainAccess.runtimeReady) return;
|
||||
setActiveRoute('fifth');
|
||||
setActiveModule('knowledge');
|
||||
setWorldEntered(true);
|
||||
setDomainConnectionOpen(false);
|
||||
}}
|
||||
onOpenCodeChannel={() => {
|
||||
setDomainConnectionOpen(false);
|
||||
setStorageSheetInitialMode('server');
|
||||
setStorageSheetOpen(true);
|
||||
}}
|
||||
/>
|
||||
<HumanSettings
|
||||
open={humanSettingsOpen}
|
||||
preferences={humanPreferences}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react';
|
||||
import { projectDomainConnectionSteps, type DomainAccessProjection } from '../domain-connection';
|
||||
|
||||
interface Props {
|
||||
access: DomainAccessProjection;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onEnterRuntime: () => void;
|
||||
onOpenCodeChannel: () => void;
|
||||
}
|
||||
|
||||
export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, onOpenCodeChannel }: Props) {
|
||||
if (!open) return null;
|
||||
const steps = projectDomainConnectionSteps(access);
|
||||
|
||||
return (
|
||||
<div className="storage-sheet-backdrop" role="presentation" onMouseDown={event => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}>
|
||||
<section className="storage-sheet domain-connection-sheet" role="dialog" aria-modal="true" aria-labelledby="domain-connection-title">
|
||||
<header className="storage-sheet-header">
|
||||
<div>
|
||||
<small>DOM-FIFTH-0001</small>
|
||||
<h2 id="domain-connection-title">接入第五域 · 光湖本源域</h2>
|
||||
</div>
|
||||
<button className="icon-button" onClick={onClose} aria-label="关闭域接入状态"><X aria-hidden="true" /></button>
|
||||
</header>
|
||||
|
||||
<p className="domain-connection-lead">进入域不是切换页面。系统必须验证账户、登记节点、目标域授权、会话能力和在线回执。</p>
|
||||
|
||||
<div className="domain-node-modes" aria-label="支持的节点类型">
|
||||
<article><Laptop aria-hidden="true" /><span><strong>本地终端节点</strong><small>使用当前联网电脑承载本地工作;正式节点登记尚未开放。</small></span></article>
|
||||
<article><Cloud aria-hidden="true" /><span><strong>云常驻节点</strong><small>服务器可持续在线;代码频道登录不等于取得域运行权限。</small></span></article>
|
||||
</div>
|
||||
|
||||
<ol className="domain-connection-steps">
|
||||
{steps.map((step, index) => (
|
||||
<li key={step.id} data-state={step.state}>
|
||||
<span>{step.state === 'verified' ? <ShieldCheck aria-hidden="true" /> : index + 1}</span>
|
||||
<div><strong>{step.label}</strong><small>{step.detail}</small></div>
|
||||
<em>{step.state === 'verified' ? '已验证' : step.state === 'checking' ? '核对中' : '待完成'}</em>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<p className="domain-connection-boundary">代码频道账号只用于仓库与同步,是节点身份的辅助证明之一,不能单独签发第五域权限。</p>
|
||||
|
||||
<footer className="storage-sheet-footer">
|
||||
<button className="secondary-button" onClick={onOpenCodeChannel}>代码频道与同步设置</button>
|
||||
<span />
|
||||
<button className="secondary-button" onClick={onClose}>返回门厅</button>
|
||||
<button className="primary-button" onClick={onEnterRuntime} disabled={!access.runtimeReady}>
|
||||
{access.runtimeReady ? '进入第五域运行体' : '等待可信域接入'}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +284,7 @@ export function StorageLocationSheet({ open, apiBase, currentRemote, initialMode
|
|||
|
||||
{!session.authenticated ? (
|
||||
<div className="server-login-form">
|
||||
<p className="sheet-label">登录服务器代码频道</p>
|
||||
<p className="sheet-label">代码频道账号</p>
|
||||
<div className="login-fields">
|
||||
<input value={username} onChange={event => setUsername(event.target.value)} placeholder="账号" autoComplete="username" />
|
||||
<input type="password" value={password} onChange={event => setPassword(event.target.value)} placeholder="密码" autoComplete="current-password" onKeyDown={event => {
|
||||
|
|
@ -292,7 +292,7 @@ export function StorageLocationSheet({ open, apiBase, currentRemote, initialMode
|
|||
}} />
|
||||
<button className="secondary-button" onClick={login} disabled={busy || !username || !password}>登录</button>
|
||||
</div>
|
||||
<small>密码只用于换取 HoloLake 应用令牌,不写入知识库或 Git 地址。</small>
|
||||
<small>密码只用于换取 HoloLake 代码频道应用令牌,不写入知识库或 Git 地址;登录代码频道不等于取得目标域运行权限。</small>
|
||||
</div>
|
||||
) : (
|
||||
<div className="repository-picker">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { projectDomainConnectionSteps } from './domain-connection.js';
|
||||
|
||||
test('code-channel account absence cannot be projected as a domain runtime login', () => {
|
||||
const steps = projectDomainConnectionSteps({
|
||||
blockers: [
|
||||
'account_node_identity_missing',
|
||||
'verified_domain_manifest_missing',
|
||||
'scoped_session_capability_missing',
|
||||
'matching_connection_receipt_missing',
|
||||
],
|
||||
runtimeReady: false,
|
||||
stage: 'login-required',
|
||||
});
|
||||
assert.deepEqual(steps.map(step => step.state), ['blocked', 'blocked', 'blocked', 'blocked']);
|
||||
assert.match(steps[0].detail, /登记的本地终端节点或云常驻节点/);
|
||||
});
|
||||
|
||||
test('verified account and node remain identity-only without domain handoff evidence', () => {
|
||||
const steps = projectDomainConnectionSteps({
|
||||
blockers: [
|
||||
'verified_domain_manifest_missing',
|
||||
'scoped_session_capability_missing',
|
||||
'matching_connection_receipt_missing',
|
||||
],
|
||||
runtimeReady: false,
|
||||
stage: 'identity-verified',
|
||||
});
|
||||
assert.deepEqual(steps.map(step => step.state), ['verified', 'blocked', 'blocked', 'blocked']);
|
||||
});
|
||||
|
||||
test('runtime-ready requires all four verified steps', () => {
|
||||
const steps = projectDomainConnectionSteps({ blockers: [], runtimeReady: true, stage: 'runtime-ready' });
|
||||
assert.ok(steps.every(step => step.state === 'verified'));
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
export type DomainAccessStage = 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready';
|
||||
|
||||
export interface DomainAccessProjection {
|
||||
blockers: string[];
|
||||
runtimeReady: boolean;
|
||||
stage: DomainAccessStage;
|
||||
}
|
||||
|
||||
export interface DomainConnectionStep {
|
||||
id: 'identity' | 'manifest' | 'capability' | 'receipt';
|
||||
label: string;
|
||||
state: 'checking' | 'blocked' | 'verified';
|
||||
detail: string;
|
||||
}
|
||||
|
||||
const blockerToStep: Record<string, DomainConnectionStep['id']> = {
|
||||
account_node_identity_missing: 'identity',
|
||||
verified_domain_manifest_missing: 'manifest',
|
||||
scoped_session_capability_missing: 'capability',
|
||||
matching_connection_receipt_missing: 'receipt',
|
||||
desktop_runtime_required: 'identity',
|
||||
domain_access_probe_failed: 'identity',
|
||||
};
|
||||
|
||||
const stepCopy: Record<DomainConnectionStep['id'], { label: string; pending: string; verified: string }> = {
|
||||
identity: {
|
||||
label: '账户与登记节点',
|
||||
pending: '需要由已登记的本地终端节点或云常驻节点完成身份证明',
|
||||
verified: '账户声明和节点编号已经验证',
|
||||
},
|
||||
manifest: {
|
||||
label: '目标域签名清单',
|
||||
pending: '等待目标域可信签名人签发并验证域 manifest',
|
||||
verified: '目标域 manifest 与可信签名来源一致',
|
||||
},
|
||||
capability: {
|
||||
label: '域会话能力',
|
||||
pending: '等待签发仅允许进入当前域的短期会话能力',
|
||||
verified: '当前节点持有有效的 domain:enter 能力',
|
||||
},
|
||||
receipt: {
|
||||
label: '在线连接回执',
|
||||
pending: '等待目标域运行体返回与节点、清单一致的在线回执',
|
||||
verified: '连接回执已回读并与当前会话一致',
|
||||
},
|
||||
};
|
||||
|
||||
export function projectDomainConnectionSteps(access: DomainAccessProjection): DomainConnectionStep[] {
|
||||
const blockers = new Set(access.blockers.map(blocker => blockerToStep[blocker]).filter(Boolean));
|
||||
const ordered: DomainConnectionStep['id'][] = ['identity', 'manifest', 'capability', 'receipt'];
|
||||
return ordered.map(id => {
|
||||
const copy = stepCopy[id];
|
||||
const verified = access.runtimeReady || (!blockers.has(id) && access.stage !== 'checking');
|
||||
return {
|
||||
id,
|
||||
label: copy.label,
|
||||
state: access.stage === 'checking' ? 'checking' : verified ? 'verified' : 'blocked',
|
||||
detail: access.stage === 'checking' ? '正在核对当前接入证据' : verified ? copy.verified : copy.pending,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2903,7 +2903,7 @@ select:focus-visible {
|
|||
|
||||
.storage-sheet-backdrop {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
z-index: 1200;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
|
@ -3123,6 +3123,111 @@ select:focus-visible {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.domain-connection-sheet .storage-sheet-header > div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.domain-connection-sheet .storage-sheet-header small {
|
||||
color: var(--lake-accent);
|
||||
font-size: 9px;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
|
||||
.domain-connection-lead,
|
||||
.domain-connection-boundary {
|
||||
margin: 18px 20px 0;
|
||||
color: var(--lake-text-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.domain-node-modes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.domain-node-modes article {
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
padding: 13px;
|
||||
border: 1px solid var(--lake-border-soft);
|
||||
border-radius: 11px;
|
||||
background: rgba(7, 17, 29, .62);
|
||||
}
|
||||
|
||||
.domain-node-modes svg {
|
||||
width: 19px;
|
||||
color: var(--lake-accent);
|
||||
}
|
||||
|
||||
.domain-node-modes strong,
|
||||
.domain-node-modes small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.domain-node-modes strong { font-size: 12px; }
|
||||
.domain-node-modes small { margin-top: 5px; color: var(--lake-muted); font-size: 9px; line-height: 1.5; }
|
||||
|
||||
.domain-connection-steps {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.domain-connection-steps li {
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--lake-border-soft);
|
||||
border-radius: 10px;
|
||||
background: rgba(9, 21, 34, .7);
|
||||
}
|
||||
|
||||
.domain-connection-steps li > span {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(209, 154, 51, .12);
|
||||
color: #d9ad5f;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.domain-connection-steps li[data-state="verified"] > span {
|
||||
background: rgba(81, 200, 120, .12);
|
||||
color: var(--lake-success);
|
||||
}
|
||||
|
||||
.domain-connection-steps svg { width: 15px; }
|
||||
.domain-connection-steps strong,
|
||||
.domain-connection-steps small { display: block; }
|
||||
.domain-connection-steps strong { font-size: 11px; }
|
||||
.domain-connection-steps small { margin-top: 3px; color: var(--lake-muted); font-size: 9px; line-height: 1.45; }
|
||||
.domain-connection-steps em { color: #d9ad5f; font-size: 9px; font-style: normal; }
|
||||
.domain-connection-steps li[data-state="verified"] em { color: var(--lake-success); }
|
||||
|
||||
.domain-connection-boundary {
|
||||
margin-bottom: 18px;
|
||||
padding: 10px 12px;
|
||||
border-left: 2px solid var(--lake-accent);
|
||||
background: var(--lake-accent-soft);
|
||||
color: #a9c8d0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 660px) {
|
||||
.domain-node-modes { grid-template-columns: 1fr; }
|
||||
.domain-connection-sheet .storage-sheet-footer { flex-wrap: wrap; }
|
||||
}
|
||||
|
||||
.human-settings-backdrop {
|
||||
position: fixed;
|
||||
z-index: 110;
|
||||
|
|
|
|||
Loading…
Reference in a new issue