hololake-system-architecture/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.ts

180 lines
6.2 KiB
TypeScript

import {
evaluateDomainAccess,
verifyDomainAccessHandoffFromSnapshot,
type DomainAccessEvidence,
type DomainNodeType,
type DomainAccessStatus,
} from './domain-access.js';
import {
TrustedSignerSnapshotLoader,
type TrustedSignerSnapshotReceipt,
} from './trusted-signer-snapshot.js';
import {
resolveNodeRegistrationEndpoint,
verifyNodeRegistrationClaim,
} from './node-registration.js';
import {
assertLoadedNodeRegistrationSnapshot,
NodeRegistrationSnapshotLoader,
type NodeRegistrationSnapshotReceipt,
} from './node-registration-snapshot.js';
import type { NodeRegistrationClaimSource } from './node-registration-client.js';
import {
createNodePossessionChallenge,
verifyNodePossessionResponse,
type NodePossessionProofSource,
} from './node-possession-proof.js';
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
export interface DomainIdentityProjection {
accountId: string;
accountVerified: boolean;
nodeId: string;
}
export interface DomainIdentitySource {
read(domainId: string, nodeType: DomainNodeType): Promise<DomainIdentityProjection>;
}
export interface DomainRuntimeHandoffCandidate {
handoff: unknown;
signerLookup: {
repositoryId: string;
signerId: string;
};
}
export interface DomainRuntimeHandoffSource {
read(domainId: string, nodeId: string, nodeType: DomainNodeType): Promise<DomainRuntimeHandoffCandidate | null>;
}
export interface DomainAccessProjection {
blockers: readonly string[];
domainId: string;
localWorkspaceAllowed: true;
nodeId: string;
nodeType: DomainNodeType;
nodeRegistrationSource: Readonly<NodeRegistrationSnapshotReceipt>;
runtimeReady: boolean;
stage: DomainAccessStatus['stage'];
trustSource: Readonly<TrustedSignerSnapshotReceipt>;
}
function safeIdentity(input: unknown): DomainIdentityProjection {
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
return { accountId: '', accountVerified: false, nodeId: '' };
}
const candidate = input as Record<string, unknown>;
const accountId = typeof candidate.accountId === 'string'
&& /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(candidate.accountId)
? candidate.accountId
: '';
const nodeId = typeof candidate.nodeId === 'string' && IDENTIFIER_PATTERN.test(candidate.nodeId)
? candidate.nodeId
: '';
return {
accountId,
accountVerified: candidate.accountVerified === true && Boolean(accountId) && Boolean(nodeId),
nodeId,
};
}
export class DomainAccessOrchestrator {
constructor(
private readonly signerSnapshots: TrustedSignerSnapshotLoader,
private readonly nodeRegistrationSnapshots: NodeRegistrationSnapshotLoader,
private readonly identities: DomainIdentitySource,
private readonly nodeRegistrations: NodeRegistrationClaimSource,
private readonly nodePossessions: NodePossessionProofSource,
private readonly handoffs: DomainRuntimeHandoffSource,
private readonly clock: () => number = Date.now,
) {}
async domainAccess(domainId: string, nodeType: DomainNodeType, at?: number): Promise<Readonly<DomainAccessProjection>> {
let now = at ?? this.clock();
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, nodeRegistrationSnapshot, identity] = await Promise.all([
this.signerSnapshots.refresh(),
this.nodeRegistrationSnapshots.refresh(),
this.identities.read(domainId, nodeType).then(safeIdentity, () => safeIdentity(null)),
]);
let nodeRegistrationVerified = false;
let nodePossessionVerified = false;
if (identity.accountVerified && nodeRegistrationSnapshot.receipt.status === 'CURRENT') {
try {
const loaded = assertLoadedNodeRegistrationSnapshot(nodeRegistrationSnapshot);
const endpoint = loaded.registry && resolveNodeRegistrationEndpoint(loaded.registry, { domainId, nodeType });
if (endpoint) {
const claim = await this.nodeRegistrations.read(Object.freeze({
accountId: identity.accountId,
domainId,
endpoint,
nodeId: identity.nodeId,
nodeType,
}));
if (claim) {
const registration = verifyNodeRegistrationClaim(claim, {
accountId: identity.accountId,
domainId,
nodeId: identity.nodeId,
nodeType,
}, endpoint, now);
nodeRegistrationVerified = true;
const challenge = createNodePossessionChallenge(registration, now);
const response = await this.nodePossessions.prove(challenge);
if (response) {
if (at === undefined) now = this.clock();
verifyNodePossessionResponse(response, challenge, registration, now);
nodePossessionVerified = true;
}
}
}
} catch {
nodeRegistrationVerified = false;
}
}
const evidence: DomainAccessEvidence = {
accountVerified: identity.accountVerified,
domainId,
nodeId: identity.nodeId,
nodePossessionVerified,
nodeRegistrationVerified,
nodeType,
};
if (identity.accountVerified && nodeRegistrationVerified && nodePossessionVerified) {
let candidate: DomainRuntimeHandoffCandidate | null = null;
try {
candidate = await this.handoffs.read(domainId, identity.nodeId, nodeType);
} catch {
candidate = null;
}
if (candidate) {
try {
const verified = verifyDomainAccessHandoffFromSnapshot(
candidate.handoff,
domainId,
identity.nodeId,
nodeType,
candidate.signerLookup,
snapshot,
now,
);
Object.assign(evidence, verified.handoff);
} catch {
// Untrusted or incomplete handoffs remain absent from the projection.
}
}
}
const status = evaluateDomainAccess(evidence, now);
return Object.freeze({
...status,
blockers: Object.freeze([...status.blockers]),
nodeRegistrationSource: nodeRegistrationSnapshot.receipt,
trustSource: snapshot.receipt,
});
}
}