fix(mobile): advertise mobile client capability
This commit is contained in:
@@ -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`
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"',
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user