import { createPublicKey, verify as verifySignature } from 'node:crypto'; import type { DomainNodeType } from './domain-access.js'; const REGISTRY_SCHEMA = 'gh-aios.node-registration-endpoints/v1' as const; const CLAIM_SCHEMA = 'gh-aios.node-registration-claim/v1' as const; const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/; const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/; const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/; const VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/; const SHA256_PATTERN = /^[a-f0-9]{64}$/; const MAX_CLAIM_LIFETIME_MS = 24 * 60 * 60 * 1000; const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; const registeredEndpoints = new WeakSet(); export interface NodeRegistrationEndpoint { algorithm: 'Ed25519'; domainIds: readonly string[]; endpointId: string; nodeTypes: readonly DomainNodeType[]; publicKeyPem: string; signerId: string; status: 'ACTIVE' | 'REVOKED'; url: string; } export interface NodeRegistrationEndpointRegistrySource { repositoryId: 'REPO-012'; sourceCommit: string; sourceUrl: string; } export interface NodeRegistrationEndpointRegistry { endpoints: readonly NodeRegistrationEndpoint[]; registryId: 'GH-AIOS-NODE-REGISTRATION-ENDPOINTS-001'; schema: typeof REGISTRY_SCHEMA; source: Readonly; state: 'CURRENT'; version: string; } export interface NodeRegistrationClaimPayload { accountId: string; claimId: string; domainId: string; endpointId: string; expiresAt: number; issuedAt: number; nodeId: string; nodeKeyFingerprint: string; nodeType: DomainNodeType; schema: typeof CLAIM_SCHEMA; signerId: string; } export interface VerifiedNodeRegistration extends NodeRegistrationClaimPayload { signature: string; verifiedAt: number; } export interface NodeRegistrationLookup { domainId: string; nodeType: DomainNodeType; } function invalidRegistry(): never { throw new Error('node_registration_endpoint_registry_invalid'); } function invalidClaim(): never { throw new Error('node_registration_claim_invalid'); } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function hasExactKeys(record: Record, keys: string[]): boolean { const expected = [...keys].sort(); const actual = Object.keys(record).sort(); return actual.length === expected.length && actual.every((key, index) => key === expected[index]); } function isIdentifier(value: unknown): value is string { return typeof value === 'string' && IDENTIFIER_PATTERN.test(value); } function parsePublicKey(value: unknown): string { if (typeof value !== 'string' || value.length > 4096 || value.includes('PRIVATE KEY') || !value.startsWith('-----BEGIN PUBLIC KEY-----\n') || !value.endsWith('-----END PUBLIC KEY-----\n')) invalidRegistry(); try { const key = createPublicKey({ format: 'pem', key: value }); if (key.asymmetricKeyType !== 'ed25519') invalidRegistry(); } catch { invalidRegistry(); } return value; } function parseEndpointUrl(value: unknown): string { if (typeof value !== 'string') invalidRegistry(); let url: URL; try { url = new URL(value); } catch { invalidRegistry(); } if (url.protocol !== 'https:' || url.hostname !== 'guanghulab.com' || url.port || url.username || url.password || url.search || url.hash || !url.pathname.startsWith('/api/ai/v1/')) invalidRegistry(); return url.toString(); } function parseEndpoint(input: unknown): NodeRegistrationEndpoint { if (!isRecord(input) || !hasExactKeys(input, ['algorithm', 'domainIds', 'endpointId', 'nodeTypes', 'publicKeyPem', 'signerId', 'status', 'url']) || input.algorithm !== 'Ed25519' || !isIdentifier(input.endpointId) || !isIdentifier(input.signerId) || (input.status !== 'ACTIVE' && input.status !== 'REVOKED') || !Array.isArray(input.domainIds) || input.domainIds.length === 0 || input.domainIds.some(domainId => !isIdentifier(domainId)) || new Set(input.domainIds).size !== input.domainIds.length || !Array.isArray(input.nodeTypes) || input.nodeTypes.length === 0 || input.nodeTypes.some(nodeType => nodeType !== 'local-terminal' && nodeType !== 'cloud-resident') || new Set(input.nodeTypes).size !== input.nodeTypes.length) invalidRegistry(); const endpoint = Object.freeze({ algorithm: 'Ed25519' as const, domainIds: Object.freeze([...input.domainIds] as string[]), endpointId: input.endpointId, nodeTypes: Object.freeze([...input.nodeTypes] as DomainNodeType[]), publicKeyPem: parsePublicKey(input.publicKeyPem), signerId: input.signerId, status: input.status, url: parseEndpointUrl(input.url), }); registeredEndpoints.add(endpoint); return endpoint; } function parseSource(source: NodeRegistrationEndpointRegistrySource): Readonly { if (!isRecord(source) || !hasExactKeys(source, ['repositoryId', 'sourceCommit', 'sourceUrl']) || source.repositoryId !== 'REPO-012' || typeof source.sourceCommit !== 'string' || !COMMIT_PATTERN.test(source.sourceCommit)) invalidRegistry(); const expected = `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${source.sourceCommit}/routing/node-registration-endpoints.json`; if (source.sourceUrl !== expected) invalidRegistry(); return Object.freeze({ ...source }); } export function parseNodeRegistrationEndpointRegistry( input: unknown, source: NodeRegistrationEndpointRegistrySource, ): NodeRegistrationEndpointRegistry { if (!isRecord(input) || !hasExactKeys(input, ['endpoints', 'registryId', 'schema', 'state', 'version']) || input.registryId !== 'GH-AIOS-NODE-REGISTRATION-ENDPOINTS-001' || input.schema !== REGISTRY_SCHEMA || input.state !== 'CURRENT' || typeof input.version !== 'string' || !VERSION_PATTERN.test(input.version) || !Array.isArray(input.endpoints)) invalidRegistry(); const endpoints = input.endpoints.map(parseEndpoint); if (new Set(endpoints.map(endpoint => endpoint.endpointId)).size !== endpoints.length) invalidRegistry(); const activeScopes = new Set(); for (const endpoint of endpoints.filter(candidate => candidate.status === 'ACTIVE')) { for (const domainId of endpoint.domainIds) { for (const nodeType of endpoint.nodeTypes) { const scope = `${domainId}\0${nodeType}`; if (activeScopes.has(scope)) invalidRegistry(); activeScopes.add(scope); } } } return Object.freeze({ endpoints: Object.freeze(endpoints), registryId: input.registryId, schema: input.schema, source: parseSource(source), state: input.state, version: input.version, }); } export function resolveNodeRegistrationEndpoint( registry: NodeRegistrationEndpointRegistry, lookup: NodeRegistrationLookup, ): NodeRegistrationEndpoint | null { const endpoint = registry.endpoints.find(candidate => candidate.status === 'ACTIVE' && candidate.domainIds.includes(lookup.domainId) && candidate.nodeTypes.includes(lookup.nodeType)); if (!endpoint) return null; if (!registeredEndpoints.has(endpoint)) throw new Error('node_registration_endpoint_unregistered'); return endpoint; } export function nodeRegistrationClaimSigningBytes(payload: NodeRegistrationClaimPayload): Buffer { return Buffer.from(JSON.stringify({ accountId: payload.accountId, claimId: payload.claimId, domainId: payload.domainId, endpointId: payload.endpointId, expiresAt: payload.expiresAt, issuedAt: payload.issuedAt, nodeId: payload.nodeId, nodeKeyFingerprint: payload.nodeKeyFingerprint, nodeType: payload.nodeType, schema: payload.schema, signerId: payload.signerId, }), 'utf8'); } export function verifyNodeRegistrationClaim( input: unknown, expected: Readonly<{ accountId: string; domainId: string; nodeId: string; nodeType: DomainNodeType }>, endpoint: NodeRegistrationEndpoint, now = Date.now(), ): Readonly { if (!registeredEndpoints.has(endpoint) || endpoint.status !== 'ACTIVE') invalidClaim(); if (!isRecord(input) || !hasExactKeys(input, ['accountId', 'claimId', 'domainId', 'endpointId', 'expiresAt', 'issuedAt', 'nodeId', 'nodeKeyFingerprint', 'nodeType', 'schema', 'signature', 'signerId'])) invalidClaim(); if (input.schema !== CLAIM_SCHEMA || typeof input.accountId !== 'string' || !ACCOUNT_ID_PATTERN.test(input.accountId) || input.accountId !== expected.accountId || !isIdentifier(input.claimId) || input.domainId !== expected.domainId || input.nodeId !== expected.nodeId || input.nodeType !== expected.nodeType || input.endpointId !== endpoint.endpointId || input.signerId !== endpoint.signerId || !endpoint.domainIds.includes(expected.domainId) || !endpoint.nodeTypes.includes(expected.nodeType) || typeof input.nodeKeyFingerprint !== 'string' || !SHA256_PATTERN.test(input.nodeKeyFingerprint) || typeof input.issuedAt !== 'number' || !Number.isSafeInteger(input.issuedAt) || typeof input.expiresAt !== 'number' || !Number.isSafeInteger(input.expiresAt) || input.issuedAt > now + MAX_CLOCK_SKEW_MS || input.expiresAt <= now || input.expiresAt <= input.issuedAt || input.expiresAt - input.issuedAt > MAX_CLAIM_LIFETIME_MS || typeof input.signature !== 'string') invalidClaim(); const payload = input as unknown as NodeRegistrationClaimPayload; let signature: Buffer; try { signature = Buffer.from(input.signature, 'base64'); if (signature.length !== 64 || signature.toString('base64') !== input.signature) invalidClaim(); if (!verifySignature(null, nodeRegistrationClaimSigningBytes(payload), endpoint.publicKeyPem, signature)) invalidClaim(); } catch { invalidClaim(); } return Object.freeze({ ...payload, signature: input.signature, verifiedAt: now, }); }