feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
Join codes need no passkey touch and carry only public values (genesis fingerprint, owner credentials, installer hashes). Nodes pin the genesis and owners at enrollment and only follow passkey-signed operator set versions. The panel's server key is listed in the set; pinned nodes require its signature and run the control link under AES-256-GCM. Password sessions carry no capabilities. The updater adopts a next_key from a signed manifest. Bump version to 0.2.0. [CI SKIP]
This commit is contained in:
1 parent
9bb6841dce
commit
a2f88a344b
89 files changed
+2145
-1195
No files matched your search
@@ -105,66 +105,63 @@ export function CapabilityPicker({ catalog, mine, value, onChange }: { catalog:
|
||||
const allOn = all.length > 0 && all.every((id) => value.has(id))
|
||||
|
||||
return (
|
||||
<div className="stack" style={{ gap: 10 }}>
|
||||
<div className="row wrap" style={{ gap: 6 }}>
|
||||
<span className="muted">预设:</span>
|
||||
{catalog.presets.map((p) => (
|
||||
<button key={p.id} type="button" className="btn small" onClick={() => set(p.capabilities.filter(selectable))}>
|
||||
{PRESETS[p.id] ?? p.id}
|
||||
<div>
|
||||
<div className="cap-toolbar stack" style={{ gap: 8 }}>
|
||||
<div className="row wrap" style={{ gap: 6 }}>
|
||||
<span className="muted">预设:</span>
|
||||
{catalog.presets.map((p) => (
|
||||
<button key={p.id} type="button" className="btn small" onClick={() => set(p.capabilities.filter(selectable))}>
|
||||
{PRESETS[p.id] ?? p.id}
|
||||
</button>
|
||||
))}
|
||||
<input className="input" style={{ maxWidth: 220, marginLeft: 'auto' }} placeholder="搜索能力" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
</div>
|
||||
<div className="row wrap" style={{ gap: 12 }}>
|
||||
<Check checked={allOn} partial={value.size > 0} onChange={(on) => set(on ? all : [])} label={<b>全选</b>} />
|
||||
<button type="button" className="btn small text" onClick={() => set(all.filter((id) => !value.has(id)))}>
|
||||
反选
|
||||
</button>
|
||||
))}
|
||||
<input className="input" style={{ maxWidth: 200, marginLeft: 'auto' }} placeholder="搜索能力" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
<span className="muted">已选 {value.size} / {all.length} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row wrap" style={{ gap: 12 }}>
|
||||
<Check checked={allOn} partial={value.size > 0} onChange={(on) => set(on ? all : [])} label={<b>全选</b>} />
|
||||
<button type="button" className="btn small text" onClick={() => set(all.filter((id) => !value.has(id)))}>
|
||||
反选
|
||||
</button>
|
||||
<span className="muted">已选 {value.size} 项</span>
|
||||
</div>
|
||||
{catalog.groups.map((g) => {
|
||||
const rows = g.capabilities.filter((c) => visible(c.id))
|
||||
if (rows.length === 0) return null
|
||||
const own = g.capabilities.filter((c) => selectable(c.id))
|
||||
const on = own.filter((c) => value.has(c.id))
|
||||
return (
|
||||
<div key={g.id} className="card" style={{ padding: 10 }}>
|
||||
<Check
|
||||
checked={own.length > 0 && on.length === own.length}
|
||||
partial={on.length > 0}
|
||||
disabled={own.length === 0}
|
||||
onChange={(next) => (next ? set([...value, ...own.map((c) => c.id)]) : onChange(new Set([...value].filter((id) => !own.some((c) => c.id === id)))))}
|
||||
label={<b>{GROUPS[g.id] ?? g.id}</b>}
|
||||
/>
|
||||
<div className="stack" style={{ gap: 4, marginTop: 6, paddingLeft: 22 }}>
|
||||
{rows.map((c) => (
|
||||
<div key={c.id} className="row wrap" style={{ gap: 8 }}>
|
||||
<Check
|
||||
checked={value.has(c.id)}
|
||||
disabled={!selectable(c.id)}
|
||||
onChange={(next) => toggle(c.id, next)}
|
||||
label={
|
||||
<>
|
||||
{NAMES[c.id]?.[0] ?? c.id}
|
||||
<span className="mono faint" style={{ marginLeft: 6 }}>
|
||||
{c.id}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{c.trust ? (
|
||||
<Chip tone="warn">
|
||||
<KeyRound size={12} /> 信任类
|
||||
</Chip>
|
||||
) : null}
|
||||
{c.reserved ? <Chip>预留</Chip> : null}
|
||||
{NAMES[c.id]?.[1] ? <span className="faint">{NAMES[c.id]![1]}</span> : null}
|
||||
</div>
|
||||
))}
|
||||
<div className="cap-grid">
|
||||
{catalog.groups.map((g) => {
|
||||
const rows = g.capabilities.filter((c) => visible(c.id))
|
||||
if (rows.length === 0) return null
|
||||
const own = g.capabilities.filter((c) => selectable(c.id))
|
||||
const on = own.filter((c) => value.has(c.id))
|
||||
return (
|
||||
<div key={g.id} className="cap-group">
|
||||
<Check
|
||||
checked={own.length > 0 && on.length === own.length}
|
||||
partial={on.length > 0}
|
||||
disabled={own.length === 0}
|
||||
onChange={(next) => (next ? set([...value, ...own.map((c) => c.id)]) : onChange(new Set([...value].filter((id) => !own.some((c) => c.id === id)))))}
|
||||
label={<b>{GROUPS[g.id] ?? g.id}</b>}
|
||||
/>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
{rows.map((c) => (
|
||||
<div key={c.id} className="cap-row">
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<Check checked={value.has(c.id)} disabled={!selectable(c.id)} onChange={(next) => toggle(c.id, next)} label={NAMES[c.id]?.[0] ?? c.id} />
|
||||
{c.trust ? (
|
||||
<Chip tone="warn">
|
||||
<KeyRound size={12} /> 信任类
|
||||
</Chip>
|
||||
) : null}
|
||||
{c.reserved ? <Chip>预留</Chip> : null}
|
||||
</div>
|
||||
<div className="cap-meta faint">
|
||||
<span className="mono">{c.id}</span>
|
||||
{NAMES[c.id]?.[1] ?? ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user