feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizationRevisionDelta,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
after_revision?: number;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await authorizationRevisionDelta({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
afterRevision: Number(payload.after_revision ?? -1),
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
fullAuthorizationSnapshot,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
placement_generation?: number;
|
||||
}>(request);
|
||||
return Response.json(await fullAuthorizationSnapshot({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
placementGeneration: Number(payload.placement_generation ?? -1),
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizeDeviceForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
device_id?: string;
|
||||
token_hash?: string;
|
||||
}>(request);
|
||||
const result = await authorizeDeviceForRelay({
|
||||
principal,
|
||||
deviceId: payload.device_id ?? "",
|
||||
tokenHash: payload.token_hash?.trim().toLowerCase() ?? "",
|
||||
});
|
||||
return Response.json(result, { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizePhoneRoute,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
route_handle?: string;
|
||||
phone_token_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await authorizePhoneRoute({
|
||||
principal,
|
||||
routeHandle: payload.route_handle ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
completePhoneHandoffForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
handoff_id?: string;
|
||||
phone_id?: string;
|
||||
phone_token_hash?: string;
|
||||
route_handle_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(await completePhoneHandoffForRelay({
|
||||
principal,
|
||||
handoffId: payload.handoff_id ?? "",
|
||||
phoneId: payload.phone_id ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
routeHandleHash: payload.route_handle_hash ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
consumePhoneHandoffForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
ticket?: string;
|
||||
pwa_origin?: string;
|
||||
name?: string;
|
||||
phone_ed25519_public?: string;
|
||||
phone_x25519_public?: string;
|
||||
identity_fingerprint?: string;
|
||||
}>(request);
|
||||
return Response.json(await consumePhoneHandoffForRelay({
|
||||
principal,
|
||||
ticket: payload.ticket ?? "",
|
||||
pwaOrigin: payload.pwa_origin ?? "",
|
||||
name: payload.name ?? "",
|
||||
phoneEd25519Public: payload.phone_ed25519_public ?? "",
|
||||
phoneX25519Public: payload.phone_x25519_public ?? "",
|
||||
identityFingerprint: payload.identity_fingerprint ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
heartbeatRelayNode,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { relayMigrationAssignments } from "@/db/relay-migrations";
|
||||
import { relayPurgeAssignments } from "@/db/relay-purges";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
generation?: number;
|
||||
capacity_tenants?: number;
|
||||
}>(request);
|
||||
const heartbeat = await heartbeatRelayNode({
|
||||
principal,
|
||||
generation: Number(payload.generation ?? -1),
|
||||
capacityTenants: Number(payload.capacity_tenants ?? -1),
|
||||
});
|
||||
const [migrations, purges] = await Promise.all([
|
||||
relayMigrationAssignments(principal),
|
||||
relayPurgeAssignments(principal),
|
||||
]);
|
||||
return Response.json(
|
||||
{ ...heartbeat, migrations, purges },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { advanceRelayMigration } from "@/db/relay-migrations";
|
||||
import { apiError, readJsonMutation } from "../../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
migration_id?: string;
|
||||
action?: "quiesced" | "copied" | "switched" | "finalized" | "failed";
|
||||
backup_ref?: string;
|
||||
manifest_sha256?: string;
|
||||
error_code?: string;
|
||||
}>(request);
|
||||
if (!payload.action) {
|
||||
return Response.json(
|
||||
{ error_code: "invalid_relay_migration", message: "迁移动作无效", retryable: false },
|
||||
{ status: 400, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const migration = await advanceRelayMigration({
|
||||
principal,
|
||||
migrationId: payload.migration_id ?? "",
|
||||
action: payload.action,
|
||||
backupRef: payload.backup_ref,
|
||||
manifestSha256: payload.manifest_sha256,
|
||||
errorCode: payload.error_code,
|
||||
});
|
||||
return Response.json(
|
||||
{ migration_id: migration.id, state: migration.state, updated_at: migration.updated_at },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { advanceRelayPurge } from "@/db/relay-purges";
|
||||
import { apiError, readJsonMutation } from "../../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
purge_id?: string;
|
||||
action?: "completed" | "failed";
|
||||
evidence_sha256?: string;
|
||||
error_code?: string;
|
||||
}>(request);
|
||||
if (!payload.action) {
|
||||
return Response.json(
|
||||
{ error_code: "invalid_relay_purge", message: "租户删除动作无效", retryable: false },
|
||||
{ status: 400, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const purge = await advanceRelayPurge({
|
||||
principal,
|
||||
purgeId: payload.purge_id ?? "",
|
||||
action: payload.action,
|
||||
evidenceSha256: payload.evidence_sha256,
|
||||
errorCode: payload.error_code,
|
||||
});
|
||||
return Response.json(
|
||||
{ purge_id: purge.id, state: purge.state, updated_at: purge.updated_at },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { claimDevice, DomainError } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
bootstrap_token?: string;
|
||||
source_hash?: string;
|
||||
os?: string;
|
||||
ed25519_public?: string;
|
||||
x25519_public?: string;
|
||||
identity_fingerprint?: string;
|
||||
transport_mode?: string;
|
||||
registration_proof?: string;
|
||||
daemon_version?: string;
|
||||
registration_retry_key?: string;
|
||||
}>(request);
|
||||
const sourceHash = payload.source_hash?.trim().toLowerCase() ?? "";
|
||||
if (!/^[0-9a-f]{64}$/u.test(sourceHash)) {
|
||||
throw new DomainError("registration_rate_limited", "注册来源摘要无效", 429, true, 60);
|
||||
}
|
||||
const result = await claimDevice({
|
||||
bootstrapToken: payload.bootstrap_token ?? "",
|
||||
trustedSourceHash: sourceHash,
|
||||
os: payload.os ?? "",
|
||||
ed25519Public: payload.ed25519_public ?? "",
|
||||
x25519Public: payload.x25519_public ?? "",
|
||||
identityFingerprint: payload.identity_fingerprint ?? "",
|
||||
transportMode: payload.transport_mode ?? "",
|
||||
registrationProof: payload.registration_proof ?? "",
|
||||
daemonVersion: payload.daemon_version ?? "",
|
||||
registrationRetryKey: payload.registration_retry_key ?? "",
|
||||
});
|
||||
return Response.json(result, {
|
||||
status: 200,
|
||||
headers: { "cache-control": "no-store" },
|
||||
});
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveDeviceRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{ device_id?: string; token_hash?: string }>(request);
|
||||
return Response.json(
|
||||
await resolveDeviceRouteForRelay({
|
||||
principal,
|
||||
deviceId: payload.device_id ?? "",
|
||||
tokenHash: payload.token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveHandoffRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{ ticket?: string; pwa_origin?: string }>(request);
|
||||
return Response.json(
|
||||
await resolveHandoffRouteForRelay({
|
||||
principal,
|
||||
ticket: payload.ticket ?? "",
|
||||
pwaOrigin: payload.pwa_origin ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolvePhoneRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
route_handle?: string;
|
||||
phone_token_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await resolvePhoneRouteForRelay({
|
||||
principal,
|
||||
routeHandle: payload.route_handle ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveTenantRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
placement_generation?: number;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await resolveTenantRouteForRelay({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
placementGeneration: Number(payload.placement_generation ?? -1),
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
revokePhoneForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
phone_id?: string;
|
||||
reason?: string;
|
||||
}>(request);
|
||||
return Response.json(await revokePhoneForRelay({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
phoneId: payload.phone_id ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user