109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import {
|
|
evaluateDomainAccess,
|
|
verifyDomainAccessHandoffFromSnapshot,
|
|
type DomainAccessEvidence,
|
|
type DomainAccessStatus,
|
|
} from './domain-access.js';
|
|
import {
|
|
TrustedSignerSnapshotLoader,
|
|
type TrustedSignerSnapshotReceipt,
|
|
} from './trusted-signer-snapshot.js';
|
|
|
|
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
|
|
|
export interface DomainIdentityProjection {
|
|
accountVerified: boolean;
|
|
nodeId: string;
|
|
}
|
|
|
|
export interface DomainIdentitySource {
|
|
read(domainId: string): Promise<DomainIdentityProjection>;
|
|
}
|
|
|
|
export interface DomainRuntimeHandoffCandidate {
|
|
handoff: unknown;
|
|
signerLookup: {
|
|
repositoryId: string;
|
|
signerId: string;
|
|
};
|
|
}
|
|
|
|
export interface DomainRuntimeHandoffSource {
|
|
read(domainId: string, nodeId: string): Promise<DomainRuntimeHandoffCandidate | null>;
|
|
}
|
|
|
|
export interface DomainAccessProjection {
|
|
blockers: readonly string[];
|
|
domainId: string;
|
|
localWorkspaceAllowed: true;
|
|
nodeId: string;
|
|
runtimeReady: boolean;
|
|
stage: DomainAccessStatus['stage'];
|
|
trustSource: Readonly<TrustedSignerSnapshotReceipt>;
|
|
}
|
|
|
|
function safeIdentity(input: unknown): DomainIdentityProjection {
|
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
return { accountVerified: false, nodeId: '' };
|
|
}
|
|
const candidate = input as Record<string, unknown>;
|
|
const nodeId = typeof candidate.nodeId === 'string' && IDENTIFIER_PATTERN.test(candidate.nodeId)
|
|
? candidate.nodeId
|
|
: '';
|
|
return {
|
|
accountVerified: candidate.accountVerified === true && Boolean(nodeId),
|
|
nodeId,
|
|
};
|
|
}
|
|
|
|
export class DomainAccessOrchestrator {
|
|
constructor(
|
|
private readonly signerSnapshots: TrustedSignerSnapshotLoader,
|
|
private readonly identities: DomainIdentitySource,
|
|
private readonly handoffs: DomainRuntimeHandoffSource,
|
|
) {}
|
|
|
|
async domainAccess(domainId: string, now = Date.now()): Promise<Readonly<DomainAccessProjection>> {
|
|
if (!IDENTIFIER_PATTERN.test(domainId)) throw new Error('domain_access_domain_invalid');
|
|
const [snapshot, identity] = await Promise.all([
|
|
this.signerSnapshots.refresh(),
|
|
this.identities.read(domainId).then(safeIdentity, () => safeIdentity(null)),
|
|
]);
|
|
const evidence: DomainAccessEvidence = {
|
|
accountVerified: identity.accountVerified,
|
|
domainId,
|
|
nodeId: identity.nodeId,
|
|
};
|
|
|
|
if (identity.accountVerified) {
|
|
let candidate: DomainRuntimeHandoffCandidate | null = null;
|
|
try {
|
|
candidate = await this.handoffs.read(domainId, identity.nodeId);
|
|
} catch {
|
|
candidate = null;
|
|
}
|
|
if (candidate) {
|
|
try {
|
|
const verified = verifyDomainAccessHandoffFromSnapshot(
|
|
candidate.handoff,
|
|
domainId,
|
|
identity.nodeId,
|
|
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]),
|
|
trustSource: snapshot.receipt,
|
|
});
|
|
}
|
|
}
|