113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
import type {
|
|
DomainRuntimeHandoffCandidate,
|
|
DomainRuntimeHandoffSource,
|
|
} from './domain-access-orchestrator.js';
|
|
import {
|
|
assertVerifiedNodePossession,
|
|
type NodePossessionChallenge,
|
|
type NodePossessionResponse,
|
|
type VerifiedNodePossession,
|
|
} from './node-possession-proof.js';
|
|
import type { VerifiedNodeRegistration } from './node-registration.js';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
|
const REQUEST_SCHEMA = 'gh-aios.domain-runtime-handoff-request/v1' as const;
|
|
const RESPONSE_SCHEMA = 'gh-aios.domain-runtime-handoff-response/v1' as const;
|
|
const consumedPossessions = new WeakSet<object>();
|
|
|
|
export interface DomainRuntimeHandoffRequest {
|
|
accountId: string;
|
|
challenge: Readonly<NodePossessionChallenge>;
|
|
domainId: string;
|
|
issuedAt: number;
|
|
nodeId: string;
|
|
nodeType: VerifiedNodeRegistration['nodeType'];
|
|
registration: Readonly<VerifiedNodeRegistration>;
|
|
requestId: string;
|
|
response: Readonly<NodePossessionResponse>;
|
|
schema: typeof REQUEST_SCHEMA;
|
|
}
|
|
|
|
export interface DomainRuntimeHandoffTransport {
|
|
request(input: Readonly<DomainRuntimeHandoffRequest>): Promise<unknown>;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
|
|
const actual = Object.keys(value).sort();
|
|
const keys = [...expected].sort();
|
|
return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
|
|
}
|
|
|
|
function parseResponse(
|
|
input: unknown,
|
|
request: DomainRuntimeHandoffRequest,
|
|
): Readonly<DomainRuntimeHandoffCandidate> | null {
|
|
if (!isRecord(input)
|
|
|| !hasExactKeys(input, ['handoff', 'request_id', 'schema', 'signer_lookup', 'status'])
|
|
|| input.schema !== RESPONSE_SCHEMA
|
|
|| input.request_id !== request.requestId
|
|
|| (input.status !== 'ISSUED' && input.status !== 'NOT_AUTHORIZED')) {
|
|
return null;
|
|
}
|
|
if (input.status === 'NOT_AUTHORIZED') {
|
|
if (input.handoff !== null || input.signer_lookup !== null) return null;
|
|
return null;
|
|
}
|
|
if (!isRecord(input.handoff)
|
|
|| !isRecord(input.signer_lookup)
|
|
|| !hasExactKeys(input.signer_lookup, ['repository_id', 'signer_id'])
|
|
|| typeof input.signer_lookup.repository_id !== 'string'
|
|
|| !IDENTIFIER_PATTERN.test(input.signer_lookup.repository_id)
|
|
|| typeof input.signer_lookup.signer_id !== 'string'
|
|
|| !IDENTIFIER_PATTERN.test(input.signer_lookup.signer_id)) return null;
|
|
return Object.freeze({
|
|
handoff: input.handoff,
|
|
signerLookup: Object.freeze({
|
|
repositoryId: input.signer_lookup.repository_id,
|
|
signerId: input.signer_lookup.signer_id,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export class StrictDomainRuntimeHandoffSource implements DomainRuntimeHandoffSource {
|
|
constructor(
|
|
private readonly transport: DomainRuntimeHandoffTransport,
|
|
private readonly clock: () => number = Date.now,
|
|
private readonly requestId: () => string = () => `HANDOFF-REQ-${randomUUID().toUpperCase()}`,
|
|
) {}
|
|
|
|
async read(possession: VerifiedNodePossession): Promise<Readonly<DomainRuntimeHandoffCandidate> | null> {
|
|
try {
|
|
const verified = assertVerifiedNodePossession(possession);
|
|
if (consumedPossessions.has(verified)) return null;
|
|
const issuedAt = this.clock();
|
|
const requestId = this.requestId();
|
|
if (!Number.isSafeInteger(issuedAt)
|
|
|| issuedAt < verified.verifiedAt
|
|
|| issuedAt >= verified.challenge.expiresAt
|
|
|| issuedAt >= verified.registration.expiresAt
|
|
|| !IDENTIFIER_PATTERN.test(requestId)) return null;
|
|
const request = Object.freeze({
|
|
accountId: verified.registration.accountId,
|
|
challenge: verified.challenge,
|
|
domainId: verified.registration.domainId,
|
|
issuedAt,
|
|
nodeId: verified.registration.nodeId,
|
|
nodeType: verified.registration.nodeType,
|
|
registration: verified.registration,
|
|
requestId,
|
|
response: verified.response,
|
|
schema: REQUEST_SCHEMA,
|
|
});
|
|
consumedPossessions.add(verified);
|
|
return parseResponse(await this.transport.request(request), request);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|