Add node registration claim transport
This commit is contained in:
parent
b8680e82e1
commit
27cd1645da
8 changed files with 369 additions and 25 deletions
|
|
@ -191,11 +191,15 @@ test('projects both trust-source states without asking for claims or handoff bef
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a signed node registration without a handoff remains identity-only', async () => {
|
test('a signed node registration without a handoff remains identity-only', async () => {
|
||||||
|
let observedEndpointId = '';
|
||||||
const orchestrator = new DomainAccessOrchestrator(
|
const orchestrator = new DomainAccessOrchestrator(
|
||||||
signerLoader(),
|
signerLoader(),
|
||||||
registrationLoader(),
|
registrationLoader(),
|
||||||
loggedInIdentity,
|
loggedInIdentity,
|
||||||
validRegistrationSource,
|
{ async read(request) {
|
||||||
|
observedEndpointId = request.endpoint.endpointId;
|
||||||
|
return validRegistrationClaim();
|
||||||
|
} },
|
||||||
{ async read() { return null; } },
|
{ async read() { return null; } },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -203,6 +207,7 @@ test('a signed node registration without a handoff remains identity-only', async
|
||||||
|
|
||||||
assert.equal(status.stage, 'identity-verified');
|
assert.equal(status.stage, 'identity-verified');
|
||||||
assert.equal(status.runtimeReady, false);
|
assert.equal(status.runtimeReady, false);
|
||||||
|
assert.equal(observedEndpointId, 'GH-NODE-REG-001');
|
||||||
assert.equal(status.blockers.includes('verified_node_registration_missing'), false);
|
assert.equal(status.blockers.includes('verified_node_registration_missing'), false);
|
||||||
assert.ok(status.blockers.includes('verified_domain_manifest_missing'));
|
assert.ok(status.blockers.includes('verified_domain_manifest_missing'));
|
||||||
});
|
});
|
||||||
|
|
@ -246,13 +251,14 @@ test('current registries, signed node claim and verified handoff project runtime
|
||||||
});
|
});
|
||||||
|
|
||||||
test('source failures are reduced to safe status without raw details', async () => {
|
test('source failures are reduced to safe status without raw details', async () => {
|
||||||
|
let claimReads = 0;
|
||||||
const unavailableSigner = new TrustedSignerSnapshotLoader({ async fetchJson() { throw new Error('secret signer detail'); } });
|
const unavailableSigner = new TrustedSignerSnapshotLoader({ async fetchJson() { throw new Error('secret signer detail'); } });
|
||||||
const unavailableRegistration = new NodeRegistrationSnapshotLoader({ async fetchJson() { throw new Error('secret registration detail'); } });
|
const unavailableRegistration = new NodeRegistrationSnapshotLoader({ async fetchJson() { throw new Error('secret registration detail'); } });
|
||||||
const orchestrator = new DomainAccessOrchestrator(
|
const orchestrator = new DomainAccessOrchestrator(
|
||||||
unavailableSigner,
|
unavailableSigner,
|
||||||
unavailableRegistration,
|
unavailableRegistration,
|
||||||
loggedInIdentity,
|
loggedInIdentity,
|
||||||
{ async read() { throw new Error('secret claim detail'); } },
|
{ async read() { claimReads += 1; throw new Error('secret claim detail'); } },
|
||||||
{ async read() { throw new Error('secret handoff detail'); } },
|
{ async read() { throw new Error('secret handoff detail'); } },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -262,6 +268,7 @@ test('source failures are reduced to safe status without raw details', async ()
|
||||||
assert.equal(status.runtimeReady, false);
|
assert.equal(status.runtimeReady, false);
|
||||||
assert.equal(status.nodeRegistrationSource.status, 'UNAVAILABLE');
|
assert.equal(status.nodeRegistrationSource.status, 'UNAVAILABLE');
|
||||||
assert.equal(status.trustSource.status, 'UNAVAILABLE');
|
assert.equal(status.trustSource.status, 'UNAVAILABLE');
|
||||||
|
assert.equal(claimReads, 0);
|
||||||
assert.equal(JSON.stringify(status).includes('secret'), false);
|
assert.equal(JSON.stringify(status).includes('secret'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
NodeRegistrationSnapshotLoader,
|
NodeRegistrationSnapshotLoader,
|
||||||
type NodeRegistrationSnapshotReceipt,
|
type NodeRegistrationSnapshotReceipt,
|
||||||
} from './node-registration-snapshot.js';
|
} from './node-registration-snapshot.js';
|
||||||
|
import type { NodeRegistrationClaimSource } from './node-registration-client.js';
|
||||||
|
|
||||||
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
||||||
|
|
||||||
|
|
@ -31,19 +32,6 @@ export interface DomainIdentitySource {
|
||||||
read(domainId: string, nodeType: DomainNodeType): Promise<DomainIdentityProjection>;
|
read(domainId: string, nodeType: DomainNodeType): Promise<DomainIdentityProjection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeRegistrationClaimRequest {
|
|
||||||
accountId: string;
|
|
||||||
domainId: string;
|
|
||||||
endpointId: string;
|
|
||||||
endpointUrl: string;
|
|
||||||
nodeId: string;
|
|
||||||
nodeType: DomainNodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeRegistrationClaimSource {
|
|
||||||
read(request: Readonly<NodeRegistrationClaimRequest>): Promise<unknown | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainRuntimeHandoffCandidate {
|
export interface DomainRuntimeHandoffCandidate {
|
||||||
handoff: unknown;
|
handoff: unknown;
|
||||||
signerLookup: {
|
signerLookup: {
|
||||||
|
|
@ -113,8 +101,7 @@ export class DomainAccessOrchestrator {
|
||||||
const claim = await this.nodeRegistrations.read(Object.freeze({
|
const claim = await this.nodeRegistrations.read(Object.freeze({
|
||||||
accountId: identity.accountId,
|
accountId: identity.accountId,
|
||||||
domainId,
|
domainId,
|
||||||
endpointId: endpoint.endpointId,
|
endpoint,
|
||||||
endpointUrl: endpoint.url,
|
|
||||||
nodeId: identity.nodeId,
|
nodeId: identity.nodeId,
|
||||||
nodeType,
|
nodeType,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { generateKeyPairSync } from 'node:crypto';
|
||||||
|
import { HttpNodeRegistrationClaimSource } from './node-registration-client.js';
|
||||||
|
import {
|
||||||
|
parseNodeRegistrationEndpointRegistry,
|
||||||
|
resolveNodeRegistrationEndpoint,
|
||||||
|
} from './node-registration.js';
|
||||||
|
|
||||||
|
const NOW = 1_786_291_200_000;
|
||||||
|
const COMMIT = 'a'.repeat(40);
|
||||||
|
const { publicKey } = generateKeyPairSync('ed25519');
|
||||||
|
|
||||||
|
function registeredEndpoint() {
|
||||||
|
const registry = parseNodeRegistrationEndpointRegistry({
|
||||||
|
endpoints: [{
|
||||||
|
algorithm: 'Ed25519',
|
||||||
|
domainIds: ['DOM-FIFTH-0001'],
|
||||||
|
endpointId: 'GH-NODE-REG-001',
|
||||||
|
nodeTypes: ['local-terminal'],
|
||||||
|
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
||||||
|
signerId: 'GH-NODE-REG-SIGNER-001',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
url: 'https://guanghulab.com/api/ai/v1/node-registrations/claims',
|
||||||
|
}],
|
||||||
|
registryId: 'GH-AIOS-NODE-REGISTRATION-ENDPOINTS-001',
|
||||||
|
schema: 'gh-aios.node-registration-endpoints/v1',
|
||||||
|
state: 'CURRENT',
|
||||||
|
version: '1.0.0',
|
||||||
|
}, {
|
||||||
|
repositoryId: 'REPO-012',
|
||||||
|
sourceCommit: COMMIT,
|
||||||
|
sourceUrl: `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${COMMIT}/routing/node-registration-endpoints.json`,
|
||||||
|
});
|
||||||
|
const endpoint = resolveNodeRegistrationEndpoint(registry, {
|
||||||
|
domainId: 'DOM-FIFTH-0001',
|
||||||
|
nodeType: 'local-terminal',
|
||||||
|
});
|
||||||
|
assert.ok(endpoint);
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookup(endpoint = registeredEndpoint()) {
|
||||||
|
return {
|
||||||
|
accountId: 'bingshuo',
|
||||||
|
domainId: 'DOM-FIFTH-0001',
|
||||||
|
endpoint,
|
||||||
|
nodeId: 'LOCAL-001',
|
||||||
|
nodeType: 'local-terminal' as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseBody(status: 'ISSUED' | 'NOT_FOUND' = 'ISSUED', overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
claim: status === 'ISSUED' ? { signed: 'claim' } : null,
|
||||||
|
requestId: 'NODE-REQ-001',
|
||||||
|
schema: 'gh-aios.node-registration-claim-response/v1',
|
||||||
|
status,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonResponse(body: unknown, init: ResponseInit = {}) {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status: 200,
|
||||||
|
...init,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...init.headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('posts an exact credentialless claim lookup only to the registered endpoint', async () => {
|
||||||
|
let observedUrl = '';
|
||||||
|
let observedInit: RequestInit | undefined;
|
||||||
|
const source = new HttpNodeRegistrationClaimSource(async (url, init) => {
|
||||||
|
observedUrl = url;
|
||||||
|
observedInit = init;
|
||||||
|
return jsonResponse(responseBody());
|
||||||
|
}, () => NOW, () => 'NODE-REQ-001');
|
||||||
|
|
||||||
|
const claim = await source.read(lookup());
|
||||||
|
|
||||||
|
assert.deepEqual(claim, { signed: 'claim' });
|
||||||
|
assert.equal(observedUrl, 'https://guanghulab.com/api/ai/v1/node-registrations/claims');
|
||||||
|
assert.equal(observedInit?.method, 'POST');
|
||||||
|
assert.equal(observedInit?.credentials, 'omit');
|
||||||
|
assert.equal(observedInit?.redirect, 'manual');
|
||||||
|
assert.equal(observedInit?.cache, 'no-store');
|
||||||
|
assert.equal(observedInit?.referrerPolicy, 'no-referrer');
|
||||||
|
assert.deepEqual(JSON.parse(String(observedInit?.body)), {
|
||||||
|
accountId: 'bingshuo',
|
||||||
|
domainId: 'DOM-FIFTH-0001',
|
||||||
|
endpointId: 'GH-NODE-REG-001',
|
||||||
|
issuedAt: NOW,
|
||||||
|
nodeId: 'LOCAL-001',
|
||||||
|
nodeType: 'local-terminal',
|
||||||
|
requestId: 'NODE-REQ-001',
|
||||||
|
schema: 'gh-aios.node-registration-claim-request/v1',
|
||||||
|
});
|
||||||
|
const headers = observedInit?.headers as Record<string, string>;
|
||||||
|
assert.equal(headers.Authorization, undefined);
|
||||||
|
assert.equal(headers.Cookie, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null only for an exact NOT_FOUND response', async () => {
|
||||||
|
const source = new HttpNodeRegistrationClaimSource(
|
||||||
|
async () => jsonResponse(responseBody('NOT_FOUND')),
|
||||||
|
() => NOW,
|
||||||
|
() => 'NODE-REQ-001',
|
||||||
|
);
|
||||||
|
assert.equal(await source.read(lookup()), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects caller-created endpoints before any network request', async () => {
|
||||||
|
let called = false;
|
||||||
|
const source = new HttpNodeRegistrationClaimSource(async () => {
|
||||||
|
called = true;
|
||||||
|
return jsonResponse(responseBody());
|
||||||
|
}, () => NOW, () => 'NODE-REQ-001');
|
||||||
|
const forged = { ...registeredEndpoint(), url: 'https://guanghulab.com/api/ai/v1/other' };
|
||||||
|
|
||||||
|
await assert.rejects(source.read(lookup(forged)), /transport_invalid/);
|
||||||
|
assert.equal(called, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects redirects, non-json responses, oversized bodies and mismatched envelopes', async () => {
|
||||||
|
const cases: Array<() => Response> = [
|
||||||
|
() => new Response(null, { status: 302, headers: { Location: 'https://guanghulab.com/api/ai/v1/other' } }),
|
||||||
|
() => new Response('{}', { status: 200, headers: { 'Content-Type': 'text/plain' } }),
|
||||||
|
() => new Response('x', { status: 200, headers: { 'Content-Type': 'application/json', 'Content-Length': '65537' } }),
|
||||||
|
() => new Response(`"${'x'.repeat(65_536)}"`, { status: 200, headers: { 'Content-Type': 'application/json' } }),
|
||||||
|
() => jsonResponse(responseBody('ISSUED', { requestId: 'NODE-REQ-OTHER' })),
|
||||||
|
() => jsonResponse({ ...responseBody(), extra: true }),
|
||||||
|
() => jsonResponse(responseBody('NOT_FOUND', { claim: {} })),
|
||||||
|
];
|
||||||
|
for (const makeResponse of cases) {
|
||||||
|
const source = new HttpNodeRegistrationClaimSource(
|
||||||
|
async () => makeResponse(),
|
||||||
|
() => NOW,
|
||||||
|
() => 'NODE-REQ-001',
|
||||||
|
);
|
||||||
|
await assert.rejects(source.read(lookup()), /transport_invalid/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reduces network errors to one non-sensitive transport failure', async () => {
|
||||||
|
const source = new HttpNodeRegistrationClaimSource(async () => {
|
||||||
|
throw new Error('secret endpoint detail');
|
||||||
|
}, () => NOW, () => 'NODE-REQ-001');
|
||||||
|
await assert.rejects(source.read(lookup()), error => {
|
||||||
|
assert.equal((error as Error).message, 'node_registration_claim_transport_invalid');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import type { DomainNodeType } from './domain-access.js';
|
||||||
|
import {
|
||||||
|
assertRegisteredNodeRegistrationEndpoint,
|
||||||
|
type NodeRegistrationEndpoint,
|
||||||
|
} from './node-registration.js';
|
||||||
|
|
||||||
|
const REQUEST_SCHEMA = 'gh-aios.node-registration-claim-request/v1' as const;
|
||||||
|
const RESPONSE_SCHEMA = 'gh-aios.node-registration-claim-response/v1' as const;
|
||||||
|
const MAX_RESPONSE_BYTES = 64 * 1024;
|
||||||
|
const REQUEST_TIMEOUT_MS = 5_000;
|
||||||
|
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
||||||
|
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}$/;
|
||||||
|
|
||||||
|
export interface NodeRegistrationClaimLookup {
|
||||||
|
accountId: string;
|
||||||
|
domainId: string;
|
||||||
|
endpoint: NodeRegistrationEndpoint;
|
||||||
|
nodeId: string;
|
||||||
|
nodeType: DomainNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeRegistrationClaimRequestBody {
|
||||||
|
accountId: string;
|
||||||
|
domainId: string;
|
||||||
|
endpointId: string;
|
||||||
|
issuedAt: number;
|
||||||
|
nodeId: string;
|
||||||
|
nodeType: DomainNodeType;
|
||||||
|
requestId: string;
|
||||||
|
schema: typeof REQUEST_SCHEMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeRegistrationClaimSource {
|
||||||
|
read(lookup: Readonly<NodeRegistrationClaimLookup>): Promise<unknown | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchImplementation = (input: string, init: RequestInit) => Promise<Response>;
|
||||||
|
|
||||||
|
function invalidTransport(): never {
|
||||||
|
throw new Error('node_registration_claim_transport_invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExactKeys(record: Record<string, unknown>, 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 buildRequest(
|
||||||
|
lookup: Readonly<NodeRegistrationClaimLookup>,
|
||||||
|
now: number,
|
||||||
|
requestId: string,
|
||||||
|
): Readonly<NodeRegistrationClaimRequestBody> {
|
||||||
|
let endpoint: NodeRegistrationEndpoint;
|
||||||
|
try {
|
||||||
|
endpoint = assertRegisteredNodeRegistrationEndpoint(lookup.endpoint);
|
||||||
|
} catch {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
if (endpoint.status !== 'ACTIVE'
|
||||||
|
|| !endpoint.domainIds.includes(lookup.domainId)
|
||||||
|
|| !endpoint.nodeTypes.includes(lookup.nodeType)
|
||||||
|
|| !ACCOUNT_ID_PATTERN.test(lookup.accountId)
|
||||||
|
|| !IDENTIFIER_PATTERN.test(lookup.domainId)
|
||||||
|
|| !IDENTIFIER_PATTERN.test(lookup.nodeId)
|
||||||
|
|| !IDENTIFIER_PATTERN.test(requestId)
|
||||||
|
|| !Number.isSafeInteger(now)) invalidTransport();
|
||||||
|
return Object.freeze({
|
||||||
|
accountId: lookup.accountId,
|
||||||
|
domainId: lookup.domainId,
|
||||||
|
endpointId: endpoint.endpointId,
|
||||||
|
issuedAt: now,
|
||||||
|
nodeId: lookup.nodeId,
|
||||||
|
nodeType: lookup.nodeType,
|
||||||
|
requestId,
|
||||||
|
schema: REQUEST_SCHEMA,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLimitedBody(response: Response): Promise<Uint8Array> {
|
||||||
|
if (!response.body) invalidTransport();
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let total = 0;
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
total += value.byteLength;
|
||||||
|
if (total > MAX_RESPONSE_BYTES) invalidTransport();
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
const body = new Uint8Array(total);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
body.set(chunk, offset);
|
||||||
|
offset += chunk.byteLength;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponse(input: unknown, request: NodeRegistrationClaimRequestBody): unknown | null {
|
||||||
|
if (!isRecord(input)
|
||||||
|
|| !hasExactKeys(input, ['claim', 'requestId', 'schema', 'status'])
|
||||||
|
|| input.schema !== RESPONSE_SCHEMA
|
||||||
|
|| input.requestId !== request.requestId
|
||||||
|
|| (input.status !== 'ISSUED' && input.status !== 'NOT_FOUND')) invalidTransport();
|
||||||
|
if (input.status === 'NOT_FOUND') {
|
||||||
|
if (input.claim !== null) invalidTransport();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isRecord(input.claim)) invalidTransport();
|
||||||
|
return input.claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpNodeRegistrationClaimSource implements NodeRegistrationClaimSource {
|
||||||
|
constructor(
|
||||||
|
private readonly fetchImpl: FetchImplementation = fetch,
|
||||||
|
private readonly now: () => number = Date.now,
|
||||||
|
private readonly requestId: () => string = () => `NODE-REQ-${randomUUID().toUpperCase()}`,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async read(lookup: Readonly<NodeRegistrationClaimLookup>): Promise<unknown | null> {
|
||||||
|
const issuedAt = this.now();
|
||||||
|
const request = buildRequest(lookup, issuedAt, this.requestId());
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await this.fetchImpl(lookup.endpoint.url, {
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
cache: 'no-store',
|
||||||
|
credentials: 'omit',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
redirect: 'manual',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
if (response.status !== 200
|
||||||
|
|| response.redirected
|
||||||
|
|| !response.headers.get('content-type')?.toLowerCase().startsWith('application/json')) {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
const contentLength = response.headers.get('content-length');
|
||||||
|
if (contentLength && (!/^\d+$/.test(contentLength) || Number(contentLength) > MAX_RESPONSE_BYTES)) {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
let decoded: string;
|
||||||
|
try {
|
||||||
|
decoded = new TextDecoder('utf-8', { fatal: true }).decode(await readLimitedBody(response));
|
||||||
|
} catch {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(decoded);
|
||||||
|
} catch {
|
||||||
|
invalidTransport();
|
||||||
|
}
|
||||||
|
if (Math.abs(this.now() - issuedAt) > MAX_CLOCK_SKEW_MS) invalidTransport();
|
||||||
|
return parseResponse(parsed, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -204,6 +204,15 @@ export function resolveNodeRegistrationEndpoint(
|
||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertRegisteredNodeRegistrationEndpoint(
|
||||||
|
endpoint: NodeRegistrationEndpoint,
|
||||||
|
): NodeRegistrationEndpoint {
|
||||||
|
if (!registeredEndpoints.has(endpoint)) {
|
||||||
|
throw new Error('node_registration_endpoint_unregistered');
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
export function nodeRegistrationClaimSigningBytes(payload: NodeRegistrationClaimPayload): Buffer {
|
export function nodeRegistrationClaimSigningBytes(payload: NodeRegistrationClaimPayload): Buffer {
|
||||||
return Buffer.from(JSON.stringify({
|
return Buffer.from(JSON.stringify({
|
||||||
accountId: payload.accountId,
|
accountId: payload.accountId,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { importKnowledgeFolder } from './folder-import.js';
|
||||||
import { DomainAccessOrchestrator } from '../../guanghu-knowledge-base/server/domain-access-orchestrator.js';
|
import { DomainAccessOrchestrator } from '../../guanghu-knowledge-base/server/domain-access-orchestrator.js';
|
||||||
import { TrustedSignerSnapshotLoader } from '../../guanghu-knowledge-base/server/trusted-signer-snapshot.js';
|
import { TrustedSignerSnapshotLoader } from '../../guanghu-knowledge-base/server/trusted-signer-snapshot.js';
|
||||||
import { NodeRegistrationSnapshotLoader } from '../../guanghu-knowledge-base/server/node-registration-snapshot.js';
|
import { NodeRegistrationSnapshotLoader } from '../../guanghu-knowledge-base/server/node-registration-snapshot.js';
|
||||||
|
import { HttpNodeRegistrationClaimSource } from '../../guanghu-knowledge-base/server/node-registration-client.js';
|
||||||
|
|
||||||
// ─── 配置 ───
|
// ─── 配置 ───
|
||||||
|
|
||||||
|
|
@ -336,6 +337,8 @@ const nodeRegistrationSnapshots = new NodeRegistrationSnapshotLoader({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeRegistrationClaims = new HttpNodeRegistrationClaimSource();
|
||||||
|
|
||||||
const domainAccessOrchestrator = new DomainAccessOrchestrator(
|
const domainAccessOrchestrator = new DomainAccessOrchestrator(
|
||||||
trustedSignerSnapshots,
|
trustedSignerSnapshots,
|
||||||
nodeRegistrationSnapshots,
|
nodeRegistrationSnapshots,
|
||||||
|
|
@ -357,13 +360,7 @@ const domainAccessOrchestrator = new DomainAccessOrchestrator(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
nodeRegistrationClaims,
|
||||||
async read() {
|
|
||||||
// Current REPO-012 does not yet register a node-registration endpoint.
|
|
||||||
// Keep this transport absent until that public registry and service exist.
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
async read() {
|
async read() {
|
||||||
// 当前灯塔尚未发布签名运行体交接端点。保持关闭,不从账号身份推导运行体权限。
|
// 当前灯塔尚未发布签名运行体交接端点。保持关闭,不从账号身份推导运行体权限。
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,17 @@
|
||||||
- 当前桌面没有可调用的登记声明端点;
|
- 当前桌面没有可调用的登记声明端点;
|
||||||
- 当前没有生产节点登记签发公钥、登记声明或节点密钥挑战回执;
|
- 当前没有生产节点登记签发公钥、登记声明或节点密钥挑战回执;
|
||||||
- 因而 Forgejo 账号验证以后仍停留在“需要节点登记”,这是正确的失败关闭结果。
|
- 因而 Forgejo 账号验证以后仍停留在“需要节点登记”,这是正确的失败关闭结果。
|
||||||
|
|
||||||
|
## 登记声明读取传输 · 阶段 002
|
||||||
|
|
||||||
|
登记声明读取不是“创建节点登记”,也不携带账号密码、访问令牌、验证码或节点私钥。客户端
|
||||||
|
只在当前锚点注册表为 `CURRENT` 且解析出唯一活跃端点后,向该端点发送固定结构的无凭据
|
||||||
|
`POST`:账号标识、域、节点编号、节点类型、端点编号、一次性请求编号与签发时间。
|
||||||
|
|
||||||
|
传输必须同时满足:禁止重定向、5 秒超时、`application/json`、响应不超过 64 KiB、响应
|
||||||
|
请求编号与原请求一致。响应只允许 `ISSUED + claim` 或 `NOT_FOUND + null`;前者仍必须经过
|
||||||
|
阶段 001 的 Ed25519 声明验证,后者不能升级任何权限。HTTP 错误、未知字段、超限、绑定不
|
||||||
|
一致及解析失败全部关闭为“未验证节点登记”,并且不触发运行体交接。
|
||||||
|
|
||||||
|
当前公共锚点仍未登记端点,所以该传输在生产运行中不可达,也不会发出请求;实现存在不等于
|
||||||
|
端点服务、节点登记或域运行体已经存在。
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
- [`HOLOLAKE-INTENT-REASONING-MAP-20260809.md`](HOLOLAKE-INTENT-REASONING-MAP-20260809.md):当前产品意图、纠正、思维推理与因果链。
|
- [`HOLOLAKE-INTENT-REASONING-MAP-20260809.md`](HOLOLAKE-INTENT-REASONING-MAP-20260809.md):当前产品意图、纠正、思维推理与因果链。
|
||||||
- [`HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md`](HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md):`HLP-CURRENT-ARCH-001` 当前完整系统蓝图,覆盖 GH-AIOS 灯塔、五域独立入口、光湖本源域、节点登录与光湖桥。
|
- [`HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md`](HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md):`HLP-CURRENT-ARCH-001` 当前完整系统蓝图,覆盖 GH-AIOS 灯塔、五域独立入口、光湖本源域、节点登录与光湖桥。
|
||||||
- [`HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md`](HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md):桌面 0.8.0 已观察能力与最终规划的逐项差距;同时登记线上产品源码仍为 0.4.6 的分叉事实。
|
- [`HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md`](HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md):桌面 0.8.0 已观察能力与最终规划的逐项差距;同时登记线上产品源码仍为 0.4.6 的分叉事实。
|
||||||
- [`HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md`](HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md):账号验证与节点登记分离后的签名声明、端点注册表、失败关闭和当前生产缺口验收基线。
|
- [`HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md`](HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md):账号验证与节点登记分离后的签名声明、端点注册表、无凭据声明读取传输、失败关闭和当前生产缺口验收基线。
|
||||||
- [`HOLOLAKE-LANGUAGE-PERSONA-OS.md`](HOLOLAKE-LANGUAGE-PERSONA-OS.md):HoloLake 作为 AI 语言人格驱动操作系统的当前正式产品定位、操作系统映射、原生应用边界与实现缺口。
|
- [`HOLOLAKE-LANGUAGE-PERSONA-OS.md`](HOLOLAKE-LANGUAGE-PERSONA-OS.md):HoloLake 作为 AI 语言人格驱动操作系统的当前正式产品定位、操作系统映射、原生应用边界与实现缺口。
|
||||||
- [`HOLOLAKE-INTENT-REASONING-MAP-20260807.md`](HOLOLAKE-INTENT-REASONING-MAP-20260807.md):2026-08-07 历史意图层,继续保存形成过程;不再作为当前默认入口。
|
- [`HOLOLAKE-INTENT-REASONING-MAP-20260807.md`](HOLOLAKE-INTENT-REASONING-MAP-20260807.md):2026-08-07 历史意图层,继续保存形成过程;不再作为当前默认入口。
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue