diff --git a/docs/self-hosted-redesign/phase-9-mobile-api.md b/docs/self-hosted-redesign/phase-9-mobile-api.md index c91e0db..1b46011 100644 --- a/docs/self-hosted-redesign/phase-9-mobile-api.md +++ b/docs/self-hosted-redesign/phase-9-mobile-api.md @@ -2,7 +2,7 @@ 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ı. status: completed -last_updated: 2026-07-21 +last_updated: 2026-07-29 --- # Faz 9 — Mobil API ve instance discovery @@ -130,7 +130,26 @@ Mevcut hata kodları: - Capability listesi - 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 { @@ -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.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: +- `mobile-v1` - `instance.discovery` - `instance.branding` - `instance.localization` diff --git a/scripts/i18n-phase8-smoke.ts b/scripts/i18n-phase8-smoke.ts index a4ae67d..3b06304 100644 --- a/scripts/i18n-phase8-smoke.ts +++ b/scripts/i18n-phase8-smoke.ts @@ -7,7 +7,7 @@ import { type ApiLocalizationMetadata, } from "../server/api/v1/localization"; import { - NETA_CAPABILITIES, + NETA_CAPABILITY_DETAILS, type NetaLocalizedResponse, type NetaTranslationMutationShape, } from "../server/api/v1/contracts"; @@ -89,7 +89,7 @@ assert.equal(contract.responseContract.localizedResourceField, "localized"); assert.equal(contract.responseContract.translationsField, "translations"); 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, ); diff --git a/scripts/phase1-auth-smoke.mjs b/scripts/phase1-auth-smoke.mjs index eeed3c1..d7f55b4 100644 --- a/scripts/phase1-auth-smoke.mjs +++ b/scripts/phase1-auth-smoke.mjs @@ -102,8 +102,18 @@ try { assert.equal(publicMeta.payload.data.client.minimumSupportedVersion, "1.2.3-smoke.1"); assert.equal(publicMeta.payload.data.links.me, `${baseUrl}/api/v1/me`); 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( - publicMeta.payload.data.capabilities.find( + publicMeta.payload.data.capabilityDetails.find( (capability) => capability.id === "auth.device-pairing", ), { @@ -337,7 +347,7 @@ try { assert.equal(invalidChatRequest.headers.get("x-neta-error-code"), "VALIDATION_ERROR"); assert.match( await invalidChatRequest.text(), - /"messages" alanı boş olamaz/, + /^chat\.errors\.invalidDetailed\|messages: too_small$/, "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.match( 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", ); assert.equal( diff --git a/scripts/phase9-api-boundary.mjs b/scripts/phase9-api-boundary.mjs index 3aea14f..c903b64 100644 --- a/scripts/phase9-api-boundary.mjs +++ b/scripts/phase9-api-boundary.mjs @@ -18,6 +18,8 @@ for (const value of [ 'NETA_PROTOCOL = "neta"', "NETA_DISCOVERY_VERSION = 1", 'NETA_API_VERSION = "1"', + 'id: "mobile-v1"', + "NETA_CAPABILITY_DETAILS", '"instance.localization"', '"auth.device-pairing"', 'status: "planned"', diff --git a/server/api/v1/contracts.ts b/server/api/v1/contracts.ts index 946d39a..c009046 100644 --- a/server/api/v1/contracts.ts +++ b/server/api/v1/contracts.ts @@ -30,7 +30,8 @@ export type NetaTranslationMutationShape = Record< Record >; -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.branding", 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" }, ] as const satisfies readonly NetaCapability[]; +export const NETA_CAPABILITIES = NETA_CAPABILITY_DETAILS + .filter((capability) => capability.status === "available") + .map((capability) => capability.id); + export type NetaDiscoveryDocument = { protocol: typeof NETA_PROTOCOL; discoveryVersion: typeof NETA_DISCOVERY_VERSION; @@ -70,7 +75,8 @@ export type NetaDiscoveryDocument = { }>; catalogVersion: number; }; - capabilities: readonly NetaCapability[]; + capabilities: readonly string[]; + capabilityDetails: readonly NetaCapability[]; }; export type NetaInstanceMetadata = { @@ -136,7 +142,8 @@ export type NetaInstanceMetadata = { sessionMethod: "better-auth-cookie"; devicePairing: "planned"; }; - capabilities: readonly NetaCapability[]; + capabilities: readonly string[]; + capabilityDetails: readonly NetaCapability[]; links: { discovery: string; apiBase: string; @@ -189,6 +196,7 @@ export function buildDiscoveryDocument( catalogVersion: input.localization.catalogVersion, }, capabilities: NETA_CAPABILITIES, + capabilityDetails: NETA_CAPABILITY_DETAILS, }; } @@ -259,6 +267,7 @@ export function buildInstanceMetadata( devicePairing: "planned", }, capabilities: NETA_CAPABILITIES, + capabilityDetails: NETA_CAPABILITY_DETAILS, links: { discovery: absoluteUrl(input.appUrl, "/.well-known/neta"), apiBase: absoluteUrl(input.appUrl, NETA_API_BASE_PATH),