fix(mobile): advertise mobile client capability

This commit is contained in:
poyrazavsever
2026-07-29 12:30:36 +03:00
parent 07a0122d50
commit 13bfba1403
5 changed files with 52 additions and 11 deletions
@@ -2,7 +2,7 @@
title: Faz 9 — Mobil API, Instance Discovery ve Localization Sözleşmesi title: Faz 9 — Mobil API, Instance Discovery ve Localization Sözleşmesi
description: React Native istemcileri için discovery, API v1, metadata, localization, capability, sürümleme ve güvenlik sınırı. description: React Native istemcileri için discovery, API v1, metadata, localization, capability, sürümleme ve güvenlik sınırı.
status: completed status: completed
last_updated: 2026-07-21 last_updated: 2026-07-29
--- ---
# Faz 9 — Mobil API ve instance discovery # Faz 9 — Mobil API ve instance discovery
@@ -130,7 +130,26 @@ Mevcut hata kodları:
- Capability listesi - Capability listesi
- Discovery, API, health ve me linkleri - Discovery, API, health ve me linkleri
Capability kaydı: Mobil istemcilerin basit ve kararlı feature kontrolü yapabilmesi için
`capabilities`, yalnız kullanılabilir yetenek ID'lerini string listesi olarak
döndürür:
```json
{
"capabilities": [
"mobile-v1",
"instance.discovery",
"instance.branding"
]
}
```
`mobile-v1`, instance'ın temel Neta mobil discovery, metadata, health,
localization ve session sözleşmesini sunduğunu belirtir. Planlanmış fakat henüz
kullanılamayan yetenekler bu string listesine girmez.
Sürüm, durum ve erişim bilgisine ihtiyaç duyan istemciler aynı yanıttaki
`capabilityDetails` alanını kullanır. Ayrıntılı capability kaydı:
```json ```json
{ {
@@ -141,7 +160,7 @@ Capability kaydı:
} }
``` ```
İstemci bilinmeyen capability ID ve alanlarını yok saymalıdır. `status=planned`, endpoint'in kullanılabilir olduğu anlamına gelmez. İstemci bilinmeyen capability ID ve alanlarını yok saymalıdır. `status=planned`, endpoint'in kullanılabilir olduğu anlamına gelmez. Feature gating için string `capabilities` listesi; yönetim, debug ve ileri seviye sürüm kontrolü için `capabilityDetails` kullanılır.
`instance.workspaceName` kullanıcıya gösterilecek firma/freelance çalışma alanı adıdır. `instance.workspaceName` kullanıcıya gösterilecek firma/freelance çalışma alanı adıdır.
`instance.metaTitle` web metadata başlığıdır. `branding.lightLogoUrl`, `instance.metaTitle` web metadata başlığıdır. `branding.lightLogoUrl`,
@@ -185,6 +204,7 @@ portal varsayılanı bu endpoint ile değişmez.
İlk capability seti: İlk capability seti:
- `mobile-v1`
- `instance.discovery` - `instance.discovery`
- `instance.branding` - `instance.branding`
- `instance.localization` - `instance.localization`
+2 -2
View File
@@ -7,7 +7,7 @@ import {
type ApiLocalizationMetadata, type ApiLocalizationMetadata,
} from "../server/api/v1/localization"; } from "../server/api/v1/localization";
import { import {
NETA_CAPABILITIES, NETA_CAPABILITY_DETAILS,
type NetaLocalizedResponse, type NetaLocalizedResponse,
type NetaTranslationMutationShape, type NetaTranslationMutationShape,
} from "../server/api/v1/contracts"; } from "../server/api/v1/contracts";
@@ -89,7 +89,7 @@ assert.equal(contract.responseContract.localizedResourceField, "localized");
assert.equal(contract.responseContract.translationsField, "translations"); assert.equal(contract.responseContract.translationsField, "translations");
assert.equal( assert.equal(
NETA_CAPABILITIES.some((capability) => capability.id === "instance.localization" && capability.status === "available"), NETA_CAPABILITY_DETAILS.some((capability) => capability.id === "instance.localization" && capability.status === "available"),
true, true,
); );
+13 -3
View File
@@ -102,8 +102,18 @@ try {
assert.equal(publicMeta.payload.data.client.minimumSupportedVersion, "1.2.3-smoke.1"); assert.equal(publicMeta.payload.data.client.minimumSupportedVersion, "1.2.3-smoke.1");
assert.equal(publicMeta.payload.data.links.me, `${baseUrl}/api/v1/me`); assert.equal(publicMeta.payload.data.links.me, `${baseUrl}/api/v1/me`);
assert.deepEqual(publicMeta.payload.data.client.platforms, ["ios", "android"]); assert.deepEqual(publicMeta.payload.data.client.platforms, ["ios", "android"]);
assert.equal(
publicMeta.payload.data.capabilities.includes("mobile-v1"),
true,
"Public metadata must advertise the base mobile client contract",
);
assert.equal(
discovery.capabilities.includes("mobile-v1"),
true,
"Discovery must advertise the base mobile client contract",
);
assert.deepEqual( assert.deepEqual(
publicMeta.payload.data.capabilities.find( publicMeta.payload.data.capabilityDetails.find(
(capability) => capability.id === "auth.device-pairing", (capability) => capability.id === "auth.device-pairing",
), ),
{ {
@@ -337,7 +347,7 @@ try {
assert.equal(invalidChatRequest.headers.get("x-neta-error-code"), "VALIDATION_ERROR"); assert.equal(invalidChatRequest.headers.get("x-neta-error-code"), "VALIDATION_ERROR");
assert.match( assert.match(
await invalidChatRequest.text(), await invalidChatRequest.text(),
/"messages" alanı boş olamaz/, /^chat\.errors\.invalidDetailed\|messages: too_small$/,
"Chat validation errors must identify the invalid request field", "Chat validation errors must identify the invalid request field",
); );
@@ -353,7 +363,7 @@ try {
assert.equal(missingChatSettings.status, 400, "Missing AI key must fail: /api/chat"); assert.equal(missingChatSettings.status, 400, "Missing AI key must fail: /api/chat");
assert.match( assert.match(
await missingChatSettings.text(), await missingChatSettings.text(),
/AI sağlayıcısı ve API anahtarı/, /^chat\.errors\.invalidDetailed\|missing_settings$/,
"The AI SDK v6 transport envelope must pass request validation and reach provider settings", "The AI SDK v6 transport envelope must pass request validation and reach provider settings",
); );
assert.equal( assert.equal(
+2
View File
@@ -18,6 +18,8 @@ for (const value of [
'NETA_PROTOCOL = "neta"', 'NETA_PROTOCOL = "neta"',
"NETA_DISCOVERY_VERSION = 1", "NETA_DISCOVERY_VERSION = 1",
'NETA_API_VERSION = "1"', 'NETA_API_VERSION = "1"',
'id: "mobile-v1"',
"NETA_CAPABILITY_DETAILS",
'"instance.localization"', '"instance.localization"',
'"auth.device-pairing"', '"auth.device-pairing"',
'status: "planned"', 'status: "planned"',
+12 -3
View File
@@ -30,7 +30,8 @@ export type NetaTranslationMutationShape = Record<
Record<string, string | null> Record<string, string | null>
>; >;
export const NETA_CAPABILITIES = [ export const NETA_CAPABILITY_DETAILS = [
{ id: "mobile-v1", version: 1, status: "available", access: "public" },
{ id: "instance.discovery", version: 1, status: "available", access: "public" }, { id: "instance.discovery", version: 1, status: "available", access: "public" },
{ id: "instance.branding", version: 1, status: "available", access: "public" }, { id: "instance.branding", version: 1, status: "available", access: "public" },
{ id: "instance.localization", version: 1, status: "available", access: "public" }, { id: "instance.localization", version: 1, status: "available", access: "public" },
@@ -42,6 +43,10 @@ export const NETA_CAPABILITIES = [
{ id: "auth.device-pairing", version: 1, status: "planned", access: "freelancer" }, { id: "auth.device-pairing", version: 1, status: "planned", access: "freelancer" },
] as const satisfies readonly NetaCapability[]; ] as const satisfies readonly NetaCapability[];
export const NETA_CAPABILITIES = NETA_CAPABILITY_DETAILS
.filter((capability) => capability.status === "available")
.map((capability) => capability.id);
export type NetaDiscoveryDocument = { export type NetaDiscoveryDocument = {
protocol: typeof NETA_PROTOCOL; protocol: typeof NETA_PROTOCOL;
discoveryVersion: typeof NETA_DISCOVERY_VERSION; discoveryVersion: typeof NETA_DISCOVERY_VERSION;
@@ -70,7 +75,8 @@ export type NetaDiscoveryDocument = {
}>; }>;
catalogVersion: number; catalogVersion: number;
}; };
capabilities: readonly NetaCapability[]; capabilities: readonly string[];
capabilityDetails: readonly NetaCapability[];
}; };
export type NetaInstanceMetadata = { export type NetaInstanceMetadata = {
@@ -136,7 +142,8 @@ export type NetaInstanceMetadata = {
sessionMethod: "better-auth-cookie"; sessionMethod: "better-auth-cookie";
devicePairing: "planned"; devicePairing: "planned";
}; };
capabilities: readonly NetaCapability[]; capabilities: readonly string[];
capabilityDetails: readonly NetaCapability[];
links: { links: {
discovery: string; discovery: string;
apiBase: string; apiBase: string;
@@ -189,6 +196,7 @@ export function buildDiscoveryDocument(
catalogVersion: input.localization.catalogVersion, catalogVersion: input.localization.catalogVersion,
}, },
capabilities: NETA_CAPABILITIES, capabilities: NETA_CAPABILITIES,
capabilityDetails: NETA_CAPABILITY_DETAILS,
}; };
} }
@@ -259,6 +267,7 @@ export function buildInstanceMetadata(
devicePairing: "planned", devicePairing: "planned",
}, },
capabilities: NETA_CAPABILITIES, capabilities: NETA_CAPABILITIES,
capabilityDetails: NETA_CAPABILITY_DETAILS,
links: { links: {
discovery: absoluteUrl(input.appUrl, "/.well-known/neta"), discovery: absoluteUrl(input.appUrl, "/.well-known/neta"),
apiBase: absoluteUrl(input.appUrl, NETA_API_BASE_PATH), apiBase: absoluteUrl(input.appUrl, NETA_API_BASE_PATH),