Bind domain access to registered node type

This commit is contained in:
冰朔 2026-08-10 06:28:47 +08:00
commit 8807b99d98
15 changed files with 168 additions and 63 deletions

View file

@ -8,6 +8,7 @@ import { TrustedSignerSnapshotLoader } from './trusted-signer-snapshot.js';
const NOW = 1_786_291_200_000;
const DOMAIN_ID = 'DOM-FIFTH-0001';
const NODE_ID = 'LOCAL-001';
const NODE_TYPE = 'local-terminal' as const;
const COMMIT = 'a'.repeat(40);
const ANCHOR_URL = 'https://guanghulab.com/api/ai/v1/anchor';
const REGISTRY_URL = `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${COMMIT}/routing/trusted-domain-manifest-signers.json`;
@ -85,6 +86,7 @@ function validHandoff() {
domainId: DOMAIN_ID,
manifestDigest: digest,
nodeId: NODE_ID,
nodeType: NODE_TYPE,
receiptId: 'RECEIPT-001',
state: 'online',
},
@ -98,6 +100,7 @@ function validHandoff() {
domainId: DOMAIN_ID,
expiresAt: NOW + 60_000,
nodeId: NODE_ID,
nodeType: NODE_TYPE,
scopes: ['domain:enter'],
},
};
@ -107,11 +110,11 @@ test('projects public trust health without asking for a handoff before login', a
let handoffReads = 0;
const orchestrator = new DomainAccessOrchestrator(
loader(),
{ async read() { return { accountVerified: false, nodeId: '' }; } },
{ async read() { return { accountVerified: false, nodeId: '', nodeRegistrationVerified: false }; } },
{ async read() { handoffReads += 1; return null; } },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NOW);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(handoffReads, 0);
assert.equal(status.stage, 'login-required');
@ -128,21 +131,36 @@ test('projects public trust health without asking for a handoff before login', a
test('an authenticated node without a verified handoff remains identity-only', async () => {
const orchestrator = new DomainAccessOrchestrator(
loader(),
{ async read() { return { accountVerified: true, nodeId: NODE_ID }; } },
{ async read() { return { accountVerified: true, nodeId: NODE_ID, nodeRegistrationVerified: true }; } },
{ async read() { return null; } },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NOW);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(status.stage, 'identity-verified');
assert.equal(status.runtimeReady, false);
assert.ok(status.blockers.includes('verified_domain_manifest_missing'));
});
test('a code-channel account without node registration never contacts a domain handoff route', async () => {
let handoffReads = 0;
const orchestrator = new DomainAccessOrchestrator(
loader(),
{ async read() { return { accountVerified: true, nodeId: NODE_ID, nodeRegistrationVerified: false }; } },
{ async read() { handoffReads += 1; return validHandoff() as never; } },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(handoffReads, 0);
assert.equal(status.stage, 'login-required');
assert.ok(status.blockers.includes('verified_node_registration_missing'));
});
test('a loaded signer snapshot and verified handoff project runtime-ready', async () => {
const orchestrator = new DomainAccessOrchestrator(
loader(() => registry([activeSigner()])),
{ async read() { return { accountVerified: true, nodeId: NODE_ID }; } },
{ async read() { return { accountVerified: true, nodeId: NODE_ID, nodeRegistrationVerified: true }; } },
{ async read() {
return {
handoff: validHandoff(),
@ -151,7 +169,7 @@ test('a loaded signer snapshot and verified handoff project runtime-ready', asyn
} },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NOW);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(status.stage, 'runtime-ready');
assert.equal(status.runtimeReady, true);
@ -162,11 +180,11 @@ test('a loaded signer snapshot and verified handoff project runtime-ready', asyn
test('source and handoff failures are reduced to safe status without raw details', async () => {
const orchestrator = new DomainAccessOrchestrator(
new TrustedSignerSnapshotLoader({ async fetchJson() { throw new Error('secret source detail'); } }),
{ async read() { return { accountVerified: true, nodeId: NODE_ID }; } },
{ async read() { return { accountVerified: true, nodeId: NODE_ID, nodeRegistrationVerified: true }; } },
{ async read() { throw new Error('secret handoff detail'); } },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NOW);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(status.stage, 'identity-verified');
assert.equal(status.runtimeReady, false);
@ -182,7 +200,7 @@ test('a last-known-good signer can project access while preserving degraded heal
currentRegistry = { ...registry([activeSigner()]), version: 'invalid' };
const orchestrator = new DomainAccessOrchestrator(
signerLoader,
{ async read() { return { accountVerified: true, nodeId: NODE_ID }; } },
{ async read() { return { accountVerified: true, nodeId: NODE_ID, nodeRegistrationVerified: true }; } },
{ async read() {
return {
handoff: validHandoff(),
@ -191,7 +209,7 @@ test('a last-known-good signer can project access while preserving degraded heal
} },
);
const status = await orchestrator.domainAccess(DOMAIN_ID, NOW);
const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW);
assert.equal(status.runtimeReady, true);
assert.equal(status.trustSource.status, 'DEGRADED_LAST_KNOWN_GOOD');

View file

@ -2,6 +2,7 @@ import {
evaluateDomainAccess,
verifyDomainAccessHandoffFromSnapshot,
type DomainAccessEvidence,
type DomainNodeType,
type DomainAccessStatus,
} from './domain-access.js';
import {
@ -14,10 +15,11 @@ const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
export interface DomainIdentityProjection {
accountVerified: boolean;
nodeId: string;
nodeRegistrationVerified: boolean;
}
export interface DomainIdentitySource {
read(domainId: string): Promise<DomainIdentityProjection>;
read(domainId: string, nodeType: DomainNodeType): Promise<DomainIdentityProjection>;
}
export interface DomainRuntimeHandoffCandidate {
@ -29,7 +31,7 @@ export interface DomainRuntimeHandoffCandidate {
}
export interface DomainRuntimeHandoffSource {
read(domainId: string, nodeId: string): Promise<DomainRuntimeHandoffCandidate | null>;
read(domainId: string, nodeId: string, nodeType: DomainNodeType): Promise<DomainRuntimeHandoffCandidate | null>;
}
export interface DomainAccessProjection {
@ -37,6 +39,7 @@ export interface DomainAccessProjection {
domainId: string;
localWorkspaceAllowed: true;
nodeId: string;
nodeType: DomainNodeType;
runtimeReady: boolean;
stage: DomainAccessStatus['stage'];
trustSource: Readonly<TrustedSignerSnapshotReceipt>;
@ -44,7 +47,7 @@ export interface DomainAccessProjection {
function safeIdentity(input: unknown): DomainIdentityProjection {
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
return { accountVerified: false, nodeId: '' };
return { accountVerified: false, nodeId: '', nodeRegistrationVerified: false };
}
const candidate = input as Record<string, unknown>;
const nodeId = typeof candidate.nodeId === 'string' && IDENTIFIER_PATTERN.test(candidate.nodeId)
@ -53,6 +56,7 @@ function safeIdentity(input: unknown): DomainIdentityProjection {
return {
accountVerified: candidate.accountVerified === true && Boolean(nodeId),
nodeId,
nodeRegistrationVerified: candidate.nodeRegistrationVerified === true && Boolean(nodeId),
};
}
@ -63,22 +67,25 @@ export class DomainAccessOrchestrator {
private readonly handoffs: DomainRuntimeHandoffSource,
) {}
async domainAccess(domainId: string, now = Date.now()): Promise<Readonly<DomainAccessProjection>> {
async domainAccess(domainId: string, nodeType: DomainNodeType, now = Date.now()): Promise<Readonly<DomainAccessProjection>> {
if (!IDENTIFIER_PATTERN.test(domainId)) throw new Error('domain_access_domain_invalid');
if (nodeType !== 'local-terminal' && nodeType !== 'cloud-resident') throw new Error('domain_access_node_type_invalid');
const [snapshot, identity] = await Promise.all([
this.signerSnapshots.refresh(),
this.identities.read(domainId).then(safeIdentity, () => safeIdentity(null)),
this.identities.read(domainId, nodeType).then(safeIdentity, () => safeIdentity(null)),
]);
const evidence: DomainAccessEvidence = {
accountVerified: identity.accountVerified,
domainId,
nodeId: identity.nodeId,
nodeRegistrationVerified: identity.nodeRegistrationVerified,
nodeType,
};
if (identity.accountVerified) {
if (identity.accountVerified && identity.nodeRegistrationVerified) {
let candidate: DomainRuntimeHandoffCandidate | null = null;
try {
candidate = await this.handoffs.read(domainId, identity.nodeId);
candidate = await this.handoffs.read(domainId, identity.nodeId, nodeType);
} catch {
candidate = null;
}
@ -88,6 +95,7 @@ export class DomainAccessOrchestrator {
candidate.handoff,
domainId,
identity.nodeId,
nodeType,
candidate.signerLookup,
snapshot,
now,

View file

@ -16,6 +16,7 @@ import { TrustedSignerSnapshotLoader } from './trusted-signer-snapshot.js';
const NOW = 1_786_291_200_000;
const COMMIT = 'b'.repeat(40);
const NODE_TYPE = 'local-terminal' as const;
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
function trustedSignerFromRegistry(overrides: Record<string, unknown> = {}): TrustedManifestSigner {
const parsed = parseTrustedManifestSignerRegistry({
@ -62,6 +63,7 @@ const validHandoff = {
domainId: 'DOM-FIFTH-0001',
manifestDigest: DIGEST,
nodeId: 'LOCAL-001',
nodeType: NODE_TYPE,
receiptId: 'RECEIPT-001',
state: 'online',
},
@ -75,6 +77,7 @@ const validHandoff = {
domainId: 'DOM-FIFTH-0001',
expiresAt: NOW + 60_000,
nodeId: 'LOCAL-001',
nodeType: NODE_TYPE,
scopes: ['domain:enter'],
},
} as const;
@ -82,6 +85,7 @@ const verifiedHandoff = parseDomainAccessHandoff(
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
trustedSigner,
NOW,
);
@ -143,14 +147,21 @@ function snapshotLoader(readRegistry: () => unknown) {
}
test('a local workspace remains available without claiming domain runtime access', () => {
const status = evaluateDomainAccess({ accountVerified: false, domainId: 'DOM-FIFTH-0001', nodeId: '' }, NOW);
const status = evaluateDomainAccess({ accountVerified: false, domainId: 'DOM-FIFTH-0001', nodeId: '', nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW);
assert.equal(status.localWorkspaceAllowed, true);
assert.equal(status.runtimeReady, false);
assert.equal(status.stage, 'login-required');
});
test('a verified Forgejo account is not a domain runtime session', () => {
const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY' }, NOW);
test('a code-channel account cannot substitute for verified node registration', () => {
const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW);
assert.equal(status.stage, 'login-required');
assert.equal(status.blockers.includes('account_identity_missing'), false);
assert.ok(status.blockers.includes('verified_node_registration_missing'));
});
test('a separately verified node is still not a domain runtime session', () => {
const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodeRegistrationVerified: true, nodeType: NODE_TYPE }, NOW);
assert.equal(status.stage, 'identity-verified');
assert.deepEqual(status.blockers, [
'verified_domain_manifest_missing',
@ -160,11 +171,13 @@ test('a verified Forgejo account is not a domain runtime session', () => {
});
test('runtime access requires matching manifest, scoped capability and online receipt', () => {
const handoff = parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW);
const handoff = parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW);
const status = evaluateDomainAccess({
accountVerified: true,
domainId: 'DOM-FIFTH-0001',
nodeId: 'LOCAL-001',
nodeRegistrationVerified: true,
nodeType: NODE_TYPE,
...handoff,
}, NOW);
assert.equal(status.runtimeReady, true);
@ -178,6 +191,7 @@ test('the runtime handoff entry resolves its signer only from a loaded snapshot'
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
{ repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
snapshot,
NOW,
@ -189,6 +203,8 @@ test('the runtime handoff entry resolves its signer only from a loaded snapshot'
accountVerified: true,
domainId: 'DOM-FIFTH-0001',
nodeId: 'LOCAL-001',
nodeRegistrationVerified: true,
nodeType: NODE_TYPE,
...verified.handoff,
}, NOW).runtimeReady, true);
});
@ -199,6 +215,7 @@ test('an empty, forged or mismatched snapshot cannot open the runtime handoff',
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
{ repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
empty,
NOW,
@ -209,6 +226,7 @@ test('an empty, forged or mismatched snapshot cannot open the runtime handoff',
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
{ repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
{ ...loaded },
NOW,
@ -217,6 +235,7 @@ test('an empty, forged or mismatched snapshot cannot open the runtime handoff',
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
{ repositoryId: 'REPO-OTHER', signerId: 'GH-LIGHTHOUSE-001' },
loaded,
NOW,
@ -234,6 +253,7 @@ test('a clearly degraded last-known-good snapshot remains usable without claimin
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
{ repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
degraded,
NOW,
@ -246,11 +266,13 @@ test('a clearly degraded last-known-good snapshot remains usable without claimin
test('mismatched or expired evidence fails closed', () => {
const status = evaluateDomainAccess({
accountVerified: true,
connectionReceipt: { connectionId: 'CONN-001', domainId: 'DOMAIN-OTHER', manifestDigest: DIGEST, nodeId: 'LOCAL-001', receiptId: 'RECEIPT-001', state: 'online' },
connectionReceipt: { connectionId: 'CONN-001', domainId: 'DOMAIN-OTHER', manifestDigest: DIGEST, nodeId: 'LOCAL-001', nodeType: NODE_TYPE, receiptId: 'RECEIPT-001', state: 'online' },
domainId: 'DOM-FIFTH-0001',
manifest: verifiedHandoff.manifest,
nodeId: 'LOCAL-001',
sessionCapability: { capabilityId: 'CAP-001', domainId: 'DOM-FIFTH-0001', expiresAt: NOW, nodeId: 'LOCAL-001', scopes: ['domain:enter'] },
nodeRegistrationVerified: true,
nodeType: NODE_TYPE,
sessionCapability: { capabilityId: 'CAP-001', domainId: 'DOM-FIFTH-0001', expiresAt: NOW, nodeId: 'LOCAL-001', nodeType: NODE_TYPE, scopes: ['domain:enter'] },
}, NOW);
assert.equal(status.runtimeReady, false);
assert.ok(status.blockers.includes('scoped_session_capability_missing'));
@ -264,7 +286,7 @@ test('the input cannot supply its own verifier receipt', () => {
...validHandoff.manifest,
verifierReceipt: { state: 'verified' },
},
}, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW), /domain_access_handoff_invalid/);
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
});
test('unknown fields and malformed digests fail closed', () => {
@ -272,22 +294,23 @@ test('unknown fields and malformed digests fail closed', () => {
...validHandoff,
manifest: { ...validHandoff.manifest, digest: 'not-a-digest' },
trusted: true,
}, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW), /domain_access_handoff_invalid/);
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
});
test('the handoff is bound to the requested domain, node and manifest digest', () => {
assert.throws(() => parseDomainAccessHandoff({
...validHandoff,
connectionReceipt: { ...validHandoff.connectionReceipt, manifestDigest: 'c'.repeat(64) },
}, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW), /domain_access_handoff_invalid/);
assert.throws(() => parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'OTHER-NODE', trustedSigner, NOW), /domain_access_handoff_invalid/);
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
assert.throws(() => parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'OTHER-NODE', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
assert.throws(() => parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'LOCAL-001', 'cloud-resident', trustedSigner, NOW), /domain_access_handoff_invalid/);
});
test('manifest tampering and signatures from another key fail closed', () => {
assert.throws(() => parseDomainAccessHandoff({
...validHandoff,
manifest: { ...validHandoff.manifest, sourceCommit: 'c'.repeat(40) },
}, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW), /domain_access_handoff_invalid/);
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
const other = generateKeyPairSync('ed25519');
assert.throws(() => parseDomainAccessHandoff({
@ -296,7 +319,7 @@ test('manifest tampering and signatures from another key fail closed', () => {
...validHandoff.manifest,
signature: sign(null, signedBytes, other.privateKey).toString('base64'),
},
}, 'DOM-FIFTH-0001', 'LOCAL-001', trustedSigner, NOW), /domain_access_handoff_invalid/);
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
});
test('trusted signer identity and repository are external inputs, not payload authority', () => {
@ -304,6 +327,7 @@ test('trusted signer identity and repository are external inputs, not payload au
validHandoff,
'DOM-FIFTH-0001',
'LOCAL-001',
NODE_TYPE,
trustedSignerFromRegistry({ repositoryId: 'REPO-OTHER' }),
NOW,
), /domain_access_handoff_invalid/);
@ -319,6 +343,8 @@ test('copying verified-looking fields cannot bypass the in-process verifier boun
verifierReceipt: { ...verifiedHandoff.manifest.verifierReceipt },
},
nodeId: 'LOCAL-001',
nodeRegistrationVerified: true,
nodeType: NODE_TYPE,
sessionCapability: verifiedHandoff.sessionCapability,
}, NOW);
assert.equal(status.runtimeReady, false);

View file

@ -16,6 +16,8 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
const cryptographicallyVerifiedManifests = new WeakSet<object>();
export type DomainNodeType = 'local-terminal' | 'cloud-resident';
export interface DomainManifestEvidence {
digest: string;
domainId: string;
@ -46,6 +48,7 @@ export interface DomainSessionCapability {
domainId: string;
expiresAt: number;
nodeId: string;
nodeType: DomainNodeType;
scopes: string[];
}
@ -54,6 +57,7 @@ export interface DomainConnectionReceipt {
domainId: string;
manifestDigest: string;
nodeId: string;
nodeType: DomainNodeType;
receiptId: string;
state: 'online';
}
@ -75,6 +79,8 @@ export interface DomainAccessEvidence {
domainId: string;
manifest?: DomainManifestEvidence;
nodeId: string;
nodeRegistrationVerified: boolean;
nodeType: DomainNodeType;
sessionCapability?: DomainSessionCapability;
}
@ -83,6 +89,7 @@ export interface DomainAccessStatus {
domainId: string;
localWorkspaceAllowed: true;
nodeId: string;
nodeType: DomainNodeType;
runtimeReady: boolean;
stage: 'login-required' | 'identity-verified' | 'runtime-ready';
}
@ -123,6 +130,7 @@ export function parseDomainAccessHandoff(
input: unknown,
expectedDomainId: string,
expectedNodeId: string,
expectedNodeType: DomainNodeType,
trustedSigner: TrustedManifestSigner,
now = Date.now(),
): DomainAccessHandoff {
@ -132,8 +140,8 @@ export function parseDomainAccessHandoff(
const capability = input.sessionCapability;
const connection = input.connectionReceipt;
if (!isRecord(manifest) || !hasExactKeys(manifest, ['digest', 'domainId', 'repositoryId', 'schema', 'signature', 'signerId', 'sourceCommit'])) invalidHandoff();
if (!isRecord(capability) || !hasExactKeys(capability, ['capabilityId', 'domainId', 'expiresAt', 'nodeId', 'scopes'])) invalidHandoff();
if (!isRecord(connection) || !hasExactKeys(connection, ['connectionId', 'domainId', 'manifestDigest', 'nodeId', 'receiptId', 'state'])) invalidHandoff();
if (!isRecord(capability) || !hasExactKeys(capability, ['capabilityId', 'domainId', 'expiresAt', 'nodeId', 'nodeType', 'scopes'])) invalidHandoff();
if (!isRecord(connection) || !hasExactKeys(connection, ['connectionId', 'domainId', 'manifestDigest', 'nodeId', 'nodeType', 'receiptId', 'state'])) invalidHandoff();
const digest = manifest.digest;
if (manifest.schema !== MANIFEST_SCHEMA
@ -167,6 +175,7 @@ export function parseDomainAccessHandoff(
if (!isIdentifier(capability.capabilityId)
|| capability.domainId !== expectedDomainId
|| capability.nodeId !== expectedNodeId
|| capability.nodeType !== expectedNodeType
|| typeof capability.expiresAt !== 'number' || !Number.isSafeInteger(capability.expiresAt) || capability.expiresAt <= now
|| !Array.isArray(capability.scopes)
|| capability.scopes.some((scope) => !isIdentifier(scope))
@ -176,6 +185,7 @@ export function parseDomainAccessHandoff(
|| !isIdentifier(connection.receiptId)
|| connection.domainId !== expectedDomainId
|| connection.nodeId !== expectedNodeId
|| connection.nodeType !== expectedNodeType
|| connection.manifestDigest !== digest
|| connection.state !== 'online') invalidHandoff();
@ -201,6 +211,7 @@ export function verifyDomainAccessHandoffFromSnapshot(
input: unknown,
expectedDomainId: string,
expectedNodeId: string,
expectedNodeType: DomainNodeType,
signerLookup: Omit<TrustedSignerLookup, 'domainId'>,
snapshot: TrustedSignerSnapshotResult,
now = Date.now(),
@ -221,7 +232,7 @@ export function verifyDomainAccessHandoffFromSnapshot(
});
if (!trustedSigner) invalidHandoff();
return Object.freeze({
handoff: parseDomainAccessHandoff(input, expectedDomainId, expectedNodeId, trustedSigner, now),
handoff: parseDomainAccessHandoff(input, expectedDomainId, expectedNodeId, expectedNodeType, trustedSigner, now),
trustSource: source,
});
} catch {
@ -231,7 +242,8 @@ export function verifyDomainAccessHandoffFromSnapshot(
export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.now()): DomainAccessStatus {
const blockers: string[] = [];
if (!evidence.accountVerified || !evidence.nodeId) blockers.push('account_node_identity_missing');
if (!evidence.accountVerified) blockers.push('account_identity_missing');
if (!evidence.nodeRegistrationVerified || !evidence.nodeId) blockers.push('verified_node_registration_missing');
const manifest = evidence.manifest;
const verifier = manifest?.verifierReceipt;
if (!manifest
@ -249,6 +261,7 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
|| !capability.capabilityId
|| capability.domainId !== evidence.domainId
|| capability.nodeId !== evidence.nodeId
|| capability.nodeType !== evidence.nodeType
|| capability.expiresAt <= now
|| !capability.scopes.includes('domain:enter')) {
blockers.push('scoped_session_capability_missing');
@ -260,6 +273,7 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
|| receipt.state !== 'online'
|| receipt.domainId !== evidence.domainId
|| receipt.nodeId !== evidence.nodeId
|| receipt.nodeType !== evidence.nodeType
|| receipt.manifestDigest !== evidence.manifest?.digest) {
blockers.push('matching_connection_receipt_missing');
}
@ -269,7 +283,8 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
domainId: evidence.domainId,
localWorkspaceAllowed: true,
nodeId: evidence.nodeId,
nodeType: evidence.nodeType,
runtimeReady,
stage: runtimeReady ? 'runtime-ready' : evidence.accountVerified && Boolean(evidence.nodeId) ? 'identity-verified' : 'login-required',
stage: runtimeReady ? 'runtime-ready' : evidence.accountVerified && evidence.nodeRegistrationVerified && Boolean(evidence.nodeId) ? 'identity-verified' : 'login-required',
};
}

View file

@ -4,6 +4,7 @@ import { StrictDomainRuntimeHandoffSource } from './domain-runtime-handoff-sourc
const DOMAIN_ID = 'DOM-FIFTH-0001';
const NODE_ID = 'JD-FD-PRIMARY';
const NODE_TYPE = 'cloud-resident' as const;
function response(overrides: Record<string, unknown> = {}) {
return {
@ -26,9 +27,9 @@ test('adapts one exact endpoint-neutral response without interpreting signed evi
},
});
const candidate = await source.read(DOMAIN_ID, NODE_ID);
const candidate = await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE);
assert.deepEqual(requests, [{ domainId: DOMAIN_ID, nodeId: NODE_ID }]);
assert.deepEqual(requests, [{ domainId: DOMAIN_ID, nodeId: NODE_ID, nodeType: NODE_TYPE }]);
assert.deepEqual(candidate, {
handoff: { manifest: { digest: 'untrusted-until-orchestrator-verifies' } },
signerLookup: { repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
@ -52,7 +53,7 @@ test('rejects malformed, extended and self-authorizing response envelopes', asyn
});
for (const _sample of samples) {
assert.equal(await source.read(DOMAIN_ID, NODE_ID), null);
assert.equal(await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE), null);
}
});
@ -65,8 +66,10 @@ test('fails closed on invalid request identity and transport errors without leak
},
});
assert.equal(await source.read('bad domain', NODE_ID), null);
assert.equal(await source.read('bad domain', NODE_ID, NODE_TYPE), null);
assert.equal(requests, 0);
assert.equal(await source.read(DOMAIN_ID, NODE_ID), null);
assert.equal(await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE), null);
assert.equal(requests, 1);
assert.equal(await source.read(DOMAIN_ID, NODE_ID, 'invalid' as never), null);
assert.equal(requests, 1);
});

View file

@ -2,12 +2,14 @@ import type {
DomainRuntimeHandoffCandidate,
DomainRuntimeHandoffSource,
} from './domain-access-orchestrator.js';
import type { DomainNodeType } from './domain-access.js';
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
export interface DomainRuntimeHandoffRequest {
domainId: string;
nodeId: string;
nodeType: DomainNodeType;
}
export interface DomainRuntimeHandoffTransport {
@ -49,10 +51,11 @@ function parseResponse(input: unknown): Readonly<DomainRuntimeHandoffCandidate>
export class StrictDomainRuntimeHandoffSource implements DomainRuntimeHandoffSource {
constructor(private readonly transport: DomainRuntimeHandoffTransport) {}
async read(domainId: string, nodeId: string): Promise<Readonly<DomainRuntimeHandoffCandidate> | null> {
async read(domainId: string, nodeId: string, nodeType: DomainNodeType): Promise<Readonly<DomainRuntimeHandoffCandidate> | null> {
if (!IDENTIFIER_PATTERN.test(domainId) || !IDENTIFIER_PATTERN.test(nodeId)) return null;
if (nodeType !== 'local-terminal' && nodeType !== 'cloud-resident') return null;
try {
return parseResponse(await this.transport.request(Object.freeze({ domainId, nodeId })));
return parseResponse(await this.transport.request(Object.freeze({ domainId, nodeId, nodeType })));
} catch {
return null;
}

View file

@ -47,6 +47,7 @@ interface DomainAccessStatus {
blockers: string[];
domainId?: string;
nodeId?: string;
nodeType?: DomainNodeType;
runtimeReady: boolean;
stage: 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready';
trustSource?: {
@ -179,18 +180,18 @@ export default function App() {
}
}, []);
const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001') => {
const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001', nodeType: DomainNodeType = 'local-terminal') => {
const request = ++domainAccessRequest.current;
const server = (window as any).hololake?.server;
if (!server?.domainAccess) {
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, runtimeReady: false, stage: 'login-required' });
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, nodeType, runtimeReady: false, stage: 'login-required' });
return;
}
try {
const result = await server.domainAccess(domainId);
const result = await server.domainAccess(domainId, nodeType);
if (request === domainAccessRequest.current) setDomainAccess(result);
} catch {
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, runtimeReady: false, stage: 'login-required' });
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, nodeType, runtimeReady: false, stage: 'login-required' });
}
}, []);
@ -200,13 +201,17 @@ export default function App() {
setDomainConnectionOpen(true);
if (target) {
setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, runtimeReady: false, stage: 'checking' });
void refreshDomainAccess(target.domain.stableDomainId);
void refreshDomainAccess(target.domain.stableDomainId, target.nodeType);
}
}, [refreshDomainAccess]);
const selectDomainNodeType = useCallback((nodeType: DomainNodeType) => {
setDomainEntryTarget(current => current ? createDomainEntryTarget(current.domain.routeId, nodeType) : null);
}, []);
if (!domainEntryTarget) return;
const target = createDomainEntryTarget(domainEntryTarget.domain.routeId, nodeType);
setDomainEntryTarget(target);
setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, nodeType, runtimeReady: false, stage: 'checking' });
void refreshDomainAccess(target.domain.stableDomainId, nodeType);
}, [domainEntryTarget, refreshDomainAccess]);
const refreshChannel = useCallback(async () => {
try {

View file

@ -5,7 +5,8 @@ import { projectDomainConnectionSteps, projectDomainTrustSource } from './domain
test('code-channel account absence cannot be projected as a domain runtime login', () => {
const steps = projectDomainConnectionSteps({
blockers: [
'account_node_identity_missing',
'account_identity_missing',
'verified_node_registration_missing',
'verified_domain_manifest_missing',
'scoped_session_capability_missing',
'matching_connection_receipt_missing',

View file

@ -4,6 +4,7 @@ export interface DomainAccessProjection {
blockers: string[];
domainId?: string;
nodeId?: string;
nodeType?: 'local-terminal' | 'cloud-resident';
runtimeReady: boolean;
stage: DomainAccessStage;
trustSource?: {
@ -23,7 +24,8 @@ export interface DomainConnectionStep {
}
const blockerToStep: Record<string, DomainConnectionStep['id']> = {
account_node_identity_missing: 'identity',
account_identity_missing: 'identity',
verified_node_registration_missing: 'identity',
verified_domain_manifest_missing: 'manifest',
scoped_session_capability_missing: 'capability',
matching_connection_receipt_missing: 'receipt',

View file

@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { canEnterSelectedDomainRuntime, createDomainEntryTarget, projectDomainRuntimeBoundary } from './domain-entry-state.js';
const ready = (domainId: string) => ({ blockers: [], domainId, runtimeReady: true, stage: 'runtime-ready' as const });
const ready = (domainId: string, nodeType: 'local-terminal' | 'cloud-resident' = 'local-terminal') => ({ blockers: [], domainId, nodeType, runtimeReady: true, stage: 'runtime-ready' as const });
test('a generic login request does not silently target the Fifth Domain', () => {
assert.equal(canEnterSelectedDomainRuntime(null, ready('DOM-FIFTH-0001')), false);
@ -11,8 +11,8 @@ test('a generic login request does not silently target the Fifth Domain', () =>
test('the selected stable domain id must match the returned access evidence', () => {
const fifth = createDomainEntryTarget('fifth', 'cloud-resident');
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOMAIN-MAIN')), false);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOMAIN-MAIN')), /另一个域/);
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOMAIN-MAIN', 'cloud-resident')), false);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOMAIN-MAIN', 'cloud-resident')), /另一个域/);
});
test('enterprise vestibules cannot reuse the Fifth Domain renderer', () => {
@ -25,6 +25,8 @@ test('enterprise vestibules cannot reuse the Fifth Domain renderer', () => {
test('the Fifth Domain opens only with matching runtime-ready evidence', () => {
const fifth = createDomainEntryTarget('fifth', 'cloud-resident');
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOM-FIFTH-0001')), true);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001')), /已经匹配/);
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOM-FIFTH-0001', 'cloud-resident')), true);
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOM-FIFTH-0001', 'local-terminal')), false);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001', 'local-terminal')), /另一种节点类型/);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001', 'cloud-resident')), /已经匹配/);
});

View file

@ -17,6 +17,7 @@ export function createDomainEntryTarget(routeId: DomainRouteId, nodeType: Domain
export function canEnterSelectedDomainRuntime(target: DomainEntryTarget | null, access: DomainAccessProjection): boolean {
if (!target || !access.runtimeReady) return false;
if (access.domainId !== target.domain.stableDomainId) return false;
if (access.nodeType !== target.nodeType) return false;
// The present desktop bundle contains only the Fifth Domain renderer. The four
// enterprise domains must provide their own signed runtime package and endpoint.
return target.domain.routeId === 'fifth';
@ -27,6 +28,9 @@ export function projectDomainRuntimeBoundary(target: DomainEntryTarget | null, a
if (access.domainId && access.domainId !== target.domain.stableDomainId) {
return '回读证据属于另一个域;当前入口保持关闭。';
}
if (access.nodeType && access.nodeType !== target.nodeType) {
return '回读证据属于另一种节点类型;当前入口保持关闭并重新核验。';
}
if (target.domain.routeId !== 'fifth') {
return `${target.domain.displayName}的独立运行端点与主题包尚未登记;当前只能查看公开门厅。`;
}

View file

@ -329,14 +329,14 @@ const domainAccessOrchestrator = new DomainAccessOrchestrator(
{
async read() {
const stored = applyStoredServerAuth();
if (!stored) {
return { accountVerified: false, nodeId: defaultPersonalServerId() || '' };
}
if (!stored) return { accountVerified: false, nodeId: '', nodeRegistrationVerified: false };
try {
const { response } = await forgejoRequest(stored.nodeId, '/api/v1/user', {}, { token: stored.token });
return { accountVerified: response.ok, nodeId: stored.nodeId };
// Forgejo proves only the account part. A dedicated node-registration
// verifier must independently supply nodeRegistrationVerified.
return { accountVerified: response.ok, nodeId: stored.nodeId, nodeRegistrationVerified: false };
} catch {
return { accountVerified: false, nodeId: stored.nodeId };
return { accountVerified: false, nodeId: stored.nodeId, nodeRegistrationVerified: false };
}
},
},
@ -602,8 +602,11 @@ ipcMain.handle('server:session', async (_event, requestedNodeId?: string) => {
return { authenticated: false, nodeId };
}
});
ipcMain.handle('server:domain-access', async (_event, domainId = 'DOM-FIFTH-0001') => {
return domainAccessOrchestrator.domainAccess(String(domainId));
ipcMain.handle('server:domain-access', async (_event, input: { domainId?: unknown; nodeType?: unknown } = {}) => {
const domainId = String(input.domainId ?? 'DOM-FIFTH-0001');
const nodeType = input.nodeType === 'cloud-resident' ? 'cloud-resident' : input.nodeType === 'local-terminal' ? 'local-terminal' : null;
if (!nodeType) throw new Error('domain_access_node_type_invalid');
return domainAccessOrchestrator.domainAccess(domainId, nodeType);
});
ipcMain.handle('server:login', async (_event, input: { nodeId: string; username: string; password: string }) => {
const username = String(input.username || '').trim();

View file

@ -31,7 +31,8 @@ contextBridge.exposeInMainWorld('hololake', {
list: () => ipcRenderer.invoke('server:list'),
connect: (nodeId: string) => ipcRenderer.invoke('server:connect', nodeId),
domainRegistry: () => ipcRenderer.invoke('server:domain-registry'),
domainAccess: (domainId: string) => ipcRenderer.invoke('server:domain-access', domainId),
domainAccess: (domainId: string, nodeType: 'local-terminal' | 'cloud-resident') =>
ipcRenderer.invoke('server:domain-access', { domainId, nodeType }),
session: (nodeId?: string) => ipcRenderer.invoke('server:session', nodeId),
login: (input: { nodeId: string; username: string; password: string }) =>
ipcRenderer.invoke('server:login', input),

View file

@ -65,6 +65,7 @@ function session(overrides: Partial<DomainSessionCapability> = {}): DomainSessio
domainId: 'DOM-FIFTH-0001',
expiresAt: NOW + 60_000,
nodeId: 'LOCAL-NODE-001',
nodeType: 'LOCAL_TERMINAL_NODE',
scopes: ['domain:enter'],
...overrides,
}
@ -76,6 +77,7 @@ function receipt(overrides: Partial<DomainConnectionReceipt> = {}): DomainConnec
domainId: 'DOM-FIFTH-0001',
manifestDigest: DIGEST,
nodeId: 'LOCAL-NODE-001',
nodeType: 'LOCAL_TERMINAL_NODE',
receiptId: 'RECEIPT-001',
state: 'online',
...overrides,
@ -141,10 +143,14 @@ describe('domain runtime contract', () => {
expect(() => acceptDomainSession(access, session({ domainId: 'DOMAIN-OTHER' }), NOW))
.toThrow('domain_session_domain_mismatch')
expect(() => acceptDomainSession(access, session({ nodeType: 'CLOUD_RESIDENT_NODE' }), NOW))
.toThrow('domain_session_node_type_mismatch')
const withSession = acceptDomainSession(access, session(), NOW)
expect(() => acceptDomainConnectionReceipt(withSession, receipt({ domainId: 'DOMAIN-OTHER' })))
.toThrow('domain_connection_domain_mismatch')
expect(() => acceptDomainConnectionReceipt(withSession, receipt({ nodeType: 'CLOUD_RESIDENT_NODE' })))
.toThrow('domain_connection_node_type_mismatch')
})
it('drops execution authority and keeps an explicit read-only scene after disconnect', () => {

View file

@ -55,6 +55,7 @@ export interface DomainSessionCapability {
domainId: string
expiresAt: number
nodeId: string
nodeType: DomainNodeType
scopes: string[]
}
@ -63,6 +64,7 @@ export interface DomainConnectionReceipt {
domainId: string
manifestDigest: string
nodeId: string
nodeType: DomainNodeType
receiptId: string
state: 'online'
}
@ -217,6 +219,9 @@ export function acceptDomainSession(
if (session.expiresAt <= now) {
throw new DomainContractError('domain_session_expired')
}
if (session.nodeType !== state.nodeType) {
throw new DomainContractError('domain_session_node_type_mismatch')
}
if (!session.scopes.includes(REQUIRED_ENTER_SCOPE)) {
throw new DomainContractError('domain_session_enter_scope_missing')
}
@ -238,6 +243,9 @@ export function acceptDomainConnectionReceipt(
if (receipt.nodeId !== state.session.nodeId) {
throw new DomainContractError('domain_connection_node_mismatch')
}
if (receipt.nodeType !== state.nodeType || receipt.nodeType !== state.session.nodeType) {
throw new DomainContractError('domain_connection_node_type_mismatch')
}
if (receipt.manifestDigest !== state.manifest.provenance.digest) {
throw new DomainContractError('domain_connection_manifest_mismatch')
}