@intentcomputing/koko-agent-sdk 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +12 -5
  3. package/README.zh-CN.md +11 -4
  4. package/docs/BACKEND_AUTH_DEMOS.md +38 -0
  5. package/docs/BACKEND_AUTH_DEMOS.zh-CN.md +187 -0
  6. package/docs/INTEGRATION.md +13 -6
  7. package/docs/INTEGRATION.zh-CN.md +11 -4
  8. package/lib/commonjs/KokoAgent.js +13 -11
  9. package/lib/commonjs/KokoAgent.js.map +1 -1
  10. package/lib/commonjs/client/AgentSession.js +3 -1
  11. package/lib/commonjs/client/AgentSession.js.map +1 -1
  12. package/lib/commonjs/client/GatewayClient.js +66 -12
  13. package/lib/commonjs/client/GatewayClient.js.map +1 -1
  14. package/lib/commonjs/cursor/CursorOverlay.js +1 -2
  15. package/lib/commonjs/cursor/CursorOverlay.js.map +1 -1
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/provider/KokoProvider.js +1 -2
  18. package/lib/commonjs/provider/KokoProvider.js.map +1 -1
  19. package/lib/commonjs/provider/KokoUiContext.js +5 -7
  20. package/lib/commonjs/provider/KokoUiContext.js.map +1 -1
  21. package/lib/commonjs/ui/KokoChatSheet.js +1 -2
  22. package/lib/commonjs/ui/KokoChatSheet.js.map +1 -1
  23. package/lib/commonjs/ui/KokoComposer.js +1 -2
  24. package/lib/commonjs/ui/KokoComposer.js.map +1 -1
  25. package/lib/commonjs/ui/KokoDialogHost.js +1 -2
  26. package/lib/commonjs/ui/KokoDialogHost.js.map +1 -1
  27. package/lib/commonjs/ui/KokoObserver.js +1 -2
  28. package/lib/commonjs/ui/KokoObserver.js.map +1 -1
  29. package/lib/commonjs/ui/KokoSubtitle.js +1 -2
  30. package/lib/commonjs/ui/KokoSubtitle.js.map +1 -1
  31. package/lib/commonjs/ui/KokoTaskPanel.js +1 -2
  32. package/lib/commonjs/ui/KokoTaskPanel.js.map +1 -1
  33. package/lib/module/KokoAgent.js +12 -9
  34. package/lib/module/KokoAgent.js.map +1 -1
  35. package/lib/module/client/AgentSession.js +3 -1
  36. package/lib/module/client/AgentSession.js.map +1 -1
  37. package/lib/module/client/GatewayClient.js +66 -12
  38. package/lib/module/client/GatewayClient.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/provider/KokoUiContext.js +4 -5
  41. package/lib/module/provider/KokoUiContext.js.map +1 -1
  42. package/lib/typescript/commonjs/KokoAgent.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/client/AgentSession.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/client/GatewayClient.d.ts +24 -2
  45. package/lib/typescript/commonjs/client/GatewayClient.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/index.d.ts +1 -1
  47. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/provider/KokoUiContext.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/types.d.ts +7 -2
  50. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  51. package/lib/typescript/module/KokoAgent.d.ts.map +1 -1
  52. package/lib/typescript/module/client/AgentSession.d.ts.map +1 -1
  53. package/lib/typescript/module/client/GatewayClient.d.ts +24 -2
  54. package/lib/typescript/module/client/GatewayClient.d.ts.map +1 -1
  55. package/lib/typescript/module/index.d.ts +1 -1
  56. package/lib/typescript/module/index.d.ts.map +1 -1
  57. package/lib/typescript/module/provider/KokoUiContext.d.ts.map +1 -1
  58. package/lib/typescript/module/types.d.ts +7 -2
  59. package/lib/typescript/module/types.d.ts.map +1 -1
  60. package/package.json +3 -1
  61. package/src/KokoAgent.ts +12 -9
  62. package/src/client/AgentSession.ts +2 -0
  63. package/src/client/GatewayClient.ts +88 -14
  64. package/src/index.ts +2 -0
  65. package/src/provider/KokoUiContext.tsx +4 -5
  66. package/src/types.ts +7 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Add production `tokenProvider` authentication with expiry-aware caching and one forced refresh after a 401.
6
+ - Add host-backend token exchange documentation and Node.js, Java, Go, Python, PHP and C# examples.
7
+ - Keep static app tokens as a documented legacy development/migration path.
8
+
3
9
  ## 1.0.1
4
10
 
5
11
  - Clarify npm setup, host-provided entry controls, regional gateways, and privacy responsibilities in the published documentation.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Koko Agent SDK
2
2
 
3
- React Native SDK for an observable in-app assistant. The host supplies an app token and presents a user goal; the SDK observes the screen, asks the hosted gateway for a next step, and performs supported actions inside the app.
3
+ React Native SDK for an observable in-app assistant. The host supplies a short-lived access token and presents a user goal; the SDK observes the screen, asks the hosted gateway for a next step, and performs supported actions inside the app.
4
4
 
5
5
  [npm package](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [中文](README.zh-CN.md) · [Integration guide](docs/INTEGRATION.md) · [Privacy](docs/PRIVACY.md) · [Contributing](CONTRIBUTING.md)
6
6
 
@@ -8,7 +8,7 @@ React Native SDK for an observable in-app assistant. The host supplies an app to
8
8
 
9
9
  - React Native 0.73 or later and React 18 or later; iOS 15 or later or an Android host supporting React Native autolinking.
10
10
  - Expo development build or a native build; Expo Go cannot load this native module.
11
- - An Intent Computing app token bound to the host bundle ID or Android application ID. The app token is not an OpenAI or DeepSeek API key.
11
+ - A host backend that exchanges its server-only client secret for short-lived mobile access tokens. Neither credential is an OpenAI or DeepSeek API key.
12
12
 
13
13
  ## Install
14
14
 
@@ -21,7 +21,7 @@ Rebuild and reinstall the native app after installation or a native SDK upgrade.
21
21
 
22
22
  ## Quick start
23
23
 
24
- Mount the provider around the host navigation tree and connect `openKokoChat()` to a host button. Replace the placeholder with the token issued for your host app and environment.
24
+ Mount the provider around the host navigation tree and connect `openKokoChat()` to a host button. Supply short-lived credentials through `tokenProvider`.
25
25
 
26
26
  ```tsx
27
27
  import React from 'react';
@@ -29,8 +29,15 @@ import { Button, View } from 'react-native';
29
29
  import { KokoProvider, openKokoChat } from '@intentcomputing/koko-agent-sdk';
30
30
 
31
31
  export default function App() {
32
+ const getKokoToken = React.useCallback(async () => {
33
+ const response = await fetch('https://api.your-app.example/koko/access-token', {
34
+ headers: { Authorization: `Bearer ${await getHostSessionToken()}` },
35
+ });
36
+ if (!response.ok) throw new Error(`Koko token ${response.status}`);
37
+ return response.json(); // { accessToken, expiresAt } (Unix milliseconds)
38
+ }, []);
32
39
  return (
33
- <KokoProvider config={{ gateway: { token: 'YOUR_APP_TOKEN' }, locale: 'en' }}>
40
+ <KokoProvider config={{ gateway: { tokenProvider: getKokoToken }, locale: 'en' }}>
34
41
  <View style={{ flex: 1 }}>
35
42
  {/* Your app and navigation go here. */}
36
43
  <Button title="Open assistant" onPress={() => openKokoChat()} />
@@ -40,7 +47,7 @@ export default function App() {
40
47
  }
41
48
  ```
42
49
 
43
- The default gateway is `https://sdk.intentcomputing.net` (OpenAI). For China, set `gateway.baseUrl` to `https://sdk.intentcomputing.cn` (DeepSeek). Use a token valid for the chosen environment. Provider API keys stay on the gateway and must not be bundled into the app.
50
+ The default gateway is `https://sdk.intentcomputing.net` (OpenAI). For China, set `gateway.baseUrl` to `https://sdk.intentcomputing.cn` (DeepSeek). The SDK caches short-lived tokens, refreshes before expiry, and retries once after a 401. Long-lived client secrets and model-provider keys must never be bundled into the app. See the [backend authentication demos](docs/BACKEND_AUTH_DEMOS.md) for Node.js, Java, Go, Python, PHP and C#.
44
51
 
45
52
  Call `setKokoAppearance({ locale, theme })` when host preferences change. Configure branding through `config.branding`; custom branding is subject to the app's entitlement. See the [integration guide](docs/INTEGRATION.md) for host actions, privacy gates, and lifecycle details.
46
53
 
package/README.zh-CN.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  面向 React Native 的应用内助手 SDK。用户描述任务后,SDK 观察宿主当前页面,通过网关规划下一步,并在宿主内执行支持的操作,过程对用户可见。
4
4
 
5
- [npm 包](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [English](README.md) · [接入指南](docs/INTEGRATION.zh-CN.md) · [隐私保护](docs/PRIVACY.zh-CN.md)
5
+ [npm 包](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [English](README.md) · [接入指南](docs/INTEGRATION.zh-CN.md) · [后端鉴权 Demo](docs/BACKEND_AUTH_DEMOS.zh-CN.md) · [隐私保护](docs/PRIVACY.zh-CN.md)
6
6
 
7
7
  ## 前置条件
8
8
 
9
9
  - React Native ≥ 0.73、React ≥ 18;iOS ≥ 15,或支持原生自动链接的 Android 宿主。
10
10
  - Expo 使用开发构建或原生构建,不支持 Expo Go。
11
- - 使用绑定宿主 Bundle ID / Android Application ID 的应用 token;它不是模型 API 密钥。
11
+ - 生产环境需要宿主后端:长期 `clientSecret` 只保存在后端,App 仅取得短期 access token。它们都不是模型 API 密钥。
12
12
 
13
13
  ## 安装
14
14
 
@@ -27,9 +27,16 @@ import { Button, View } from 'react-native';
27
27
  import { KokoProvider, openKokoChat } from '@intentcomputing/koko-agent-sdk';
28
28
 
29
29
  export default function App() {
30
+ const getKokoToken = React.useCallback(async () => {
31
+ const response = await fetch('https://api.your-app.example/koko/access-token', {
32
+ headers: { Authorization: `Bearer ${await getHostSessionToken()}` },
33
+ });
34
+ if (!response.ok) throw new Error(`Koko token ${response.status}`);
35
+ return response.json(); // { accessToken, expiresAt };expiresAt 为 Unix 毫秒
36
+ }, []);
30
37
  return (
31
38
  <KokoProvider config={{
32
- gateway: { token: 'YOUR_APP_TOKEN', baseUrl: 'https://sdk.intentcomputing.cn' },
39
+ gateway: { tokenProvider: getKokoToken, baseUrl: 'https://sdk.intentcomputing.cn' },
33
40
  locale: 'zh',
34
41
  }}>
35
42
  <View style={{ flex: 1 }}>
@@ -41,7 +48,7 @@ export default function App() {
41
48
  }
42
49
  ```
43
50
 
44
- 默认网关为海外的 `https://sdk.intentcomputing.net`(OpenAI);国内请显式选择 `https://sdk.intentcomputing.cn`(DeepSeek)。使用对应环境的应用 token,模型 API 密钥只保留在网关。语言或主题变化时调用 `setKokoAppearance({ locale, theme })`;品牌配置通过 `config.branding` 传入,并受应用权益控制。
51
+ 默认网关为海外的 `https://sdk.intentcomputing.net`(OpenAI);国内请显式选择 `https://sdk.intentcomputing.cn`(DeepSeek)。SDK 会缓存短期 token、在过期前刷新,并在 401 后强制刷新一次。长期 `clientSecret` 和模型 API 密钥都不得进入 App。完整三端流程及 Node.js、Java、Go、Python、PHP、C# Demo 见[后端鉴权 Demo](docs/BACKEND_AUTH_DEMOS.zh-CN.md)。
45
52
 
46
53
  宿主必须在展示密码、私钥、助记词及其验证页面前设置隐私标记和用户操作门禁,由用户亲自完成敏感步骤。脱敏的页面信息和任务上下文会发送到所选网关进行规划,接入方应使用虚构数据验证隐私边界。详见[隐私说明](docs/PRIVACY.zh-CN.md)。
47
54
 
@@ -0,0 +1,38 @@
1
+ # Host-backend authentication demos
2
+
3
+ Production authentication uses three parties: the mobile app authenticates to its existing host backend; the host backend exchanges its server-only `clientId` and `clientSecret` with Koko; the app receives a 5–15 minute access token for the SDK.
4
+
5
+ ```text
6
+ Host app --host session--> Host backend --client secret--> Koko /v1/sdk/token
7
+ Host app <--short token--- Host backend <--short token---- Koko
8
+ Host app --short token-----------------------------------> Koko /v1/agent/*
9
+ ```
10
+
11
+ ## Rules
12
+
13
+ - Store `KOKO_CLIENT_SECRET` only in the host backend's secret manager or environment. Never return it to the app or place it in Git or logs.
14
+ - Protect and rate-limit the host `/koko/access-token` endpoint with the host's existing authentication.
15
+ - Derive `subject` on the server from the authenticated user. Send a stable pseudonymous value such as `HMAC-SHA256(hostUserId, separateSalt)`, never an email, phone number, wallet address or advertising ID.
16
+ - Use `https://sdk.intentcomputing.cn` for China and `https://sdk.intentcomputing.net` globally. Credentials and access tokens are region-bound.
17
+ - Koko returns `access_token`, `expires_in`, and `expires_at` (Unix seconds). Return `{ accessToken, expiresAt }` to the SDK, with `expiresAt` expressed in Unix milliseconds.
18
+ - Use a short timeout. Do not log authorization headers or response tokens. Rotate long-lived client secrets.
19
+
20
+ Complete, copyable examples for **Node.js/TypeScript (Express), Java (Spring Boot), Go, Python (FastAPI), PHP (Laravel), and C# (ASP.NET Core)** are maintained in the [Chinese guide](BACKEND_AUTH_DEMOS.zh-CN.md); all examples use standard HTTP Basic authentication for the server-to-server exchange and derive the subject from the host-authenticated user.
21
+
22
+ ## React Native
23
+
24
+ ```tsx
25
+ const tokenProvider = React.useCallback(async () => {
26
+ const response = await hostApi.get('/koko/access-token');
27
+ return response.data as { accessToken: string; expiresAt: number };
28
+ }, []);
29
+
30
+ <KokoProvider config={{
31
+ gateway: {
32
+ baseUrl: 'https://sdk.intentcomputing.net',
33
+ tokenProvider,
34
+ },
35
+ }}>{children}</KokoProvider>
36
+ ```
37
+
38
+ Do not persist the access token in AsyncStorage or send it to logs/analytics. The SDK keeps it in memory, refreshes shortly before expiry, and clears it before one forced `tokenProvider` retry after a 401.
@@ -0,0 +1,187 @@
1
+ # 宿主后端鉴权与多语言 Demo
2
+
3
+ 生产环境采用三端通信:宿主 App 通过自己的登录态请求宿主后端;宿主后端使用仅存于服务器的 `clientId` / `clientSecret` 向 Koko 换取 5~15 分钟的 access token;App 再把短期 token 交给 SDK。
4
+
5
+ ```text
6
+ 宿主 App ──宿主登录态──> 宿主后端 ──clientSecret──> Koko /v1/sdk/token
7
+ 宿主 App <──短期 token── 宿主后端 <──短期 token── Koko
8
+ 宿主 App ──短期 token──> Koko /v1/agent/*
9
+ ```
10
+
11
+ ## 安全约定
12
+
13
+ - `KOKO_CLIENT_SECRET` 只能放在宿主后端的密钥系统或环境变量中,不能进入 App、Git、日志或前端响应。
14
+ - 宿主的 `/koko/access-token` 必须验证已有登录态并限流,不能做成匿名转发接口。
15
+ - `subject` 必须由后端根据已认证用户生成稳定的匿名值;不要发送邮箱、手机号、钱包地址或设备广告 ID。建议 `HMAC-SHA256(宿主用户ID, 独立盐)`。
16
+ - 国内使用 `https://sdk.intentcomputing.cn`,海外使用 `https://sdk.intentcomputing.net`;凭证和 token 不跨区域使用。
17
+ - Koko 返回字段为 `access_token`、`expires_in`、`expires_at`(Unix 秒)。宿主接口应转换为 SDK 接受的 `{ accessToken, expiresAt }`,其中 `expiresAt` 是 Unix 毫秒。
18
+ - 请求 Koko 必须设置短超时,不记录 Authorization 头或响应 token。长期凭证应定期轮换。
19
+
20
+ 以下示例中的 `authenticatedUserId` 必须来自宿主已经验证的 session/JWT,不得从请求 body 或 query 直接采用。
21
+
22
+ ## Node.js / TypeScript(Express)
23
+
24
+ ```ts
25
+ import crypto from 'node:crypto';
26
+ import express from 'express';
27
+
28
+ const app = express();
29
+ const gateway = process.env.KOKO_REGION === 'cn'
30
+ ? 'https://sdk.intentcomputing.cn'
31
+ : 'https://sdk.intentcomputing.net';
32
+
33
+ app.get('/koko/access-token', requireHostLogin, async (req, res) => {
34
+ const subject = crypto.createHmac('sha256', process.env.KOKO_SUBJECT_SALT!)
35
+ .update(req.user.id) // 只取服务端认证结果
36
+ .digest('hex');
37
+ const basic = Buffer.from(
38
+ `${process.env.KOKO_CLIENT_ID}:${process.env.KOKO_CLIENT_SECRET}`,
39
+ ).toString('base64');
40
+ const upstream = await fetch(`${gateway}/v1/sdk/token`, {
41
+ method: 'POST',
42
+ headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ app_id: process.env.KOKO_APP_ID, subject }),
44
+ signal: AbortSignal.timeout(5000),
45
+ });
46
+ if (!upstream.ok) return res.status(502).json({ error: 'koko_token_unavailable' });
47
+ const token = await upstream.json();
48
+ res.set('Cache-Control', 'no-store').json({
49
+ accessToken: token.access_token,
50
+ expiresAt: token.expires_at * 1000,
51
+ });
52
+ });
53
+ ```
54
+
55
+ ## Java(Spring Boot 3)
56
+
57
+ ```java
58
+ record KokoToken(String access_token, long expires_at) {}
59
+ record AppToken(String accessToken, long expiresAt) {}
60
+
61
+ AppToken exchange(String authenticatedUserId) throws Exception {
62
+ String subject = hmacSha256Hex(System.getenv("KOKO_SUBJECT_SALT"), authenticatedUserId);
63
+ String basic = Base64.getEncoder().encodeToString(
64
+ (System.getenv("KOKO_CLIENT_ID") + ":" + System.getenv("KOKO_CLIENT_SECRET"))
65
+ .getBytes(StandardCharsets.UTF_8));
66
+ String body = objectMapper.writeValueAsString(Map.of(
67
+ "app_id", System.getenv("KOKO_APP_ID"), "subject", subject));
68
+ HttpRequest request = HttpRequest.newBuilder(URI.create(kokoGateway + "/v1/sdk/token"))
69
+ .timeout(Duration.ofSeconds(5)).header("Authorization", "Basic " + basic)
70
+ .header("Content-Type", "application/json")
71
+ .POST(HttpRequest.BodyPublishers.ofString(body)).build();
72
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
73
+ if (response.statusCode() != 200) throw new IllegalStateException("Koko token unavailable");
74
+ KokoToken token = objectMapper.readValue(response.body(), KokoToken.class);
75
+ return new AppToken(token.access_token(), token.expires_at() * 1000L);
76
+ }
77
+ ```
78
+
79
+ Spring Controller 必须从 `Principal` / SecurityContext 取得 `authenticatedUserId`,并返回 `Cache-Control: no-store`。
80
+
81
+ ## Go(net/http)
82
+
83
+ ```go
84
+ func exchangeKokoToken(ctx context.Context, authenticatedUserID string) (string, int64, error) {
85
+ mac := hmac.New(sha256.New, []byte(os.Getenv("KOKO_SUBJECT_SALT")))
86
+ mac.Write([]byte(authenticatedUserID))
87
+ body, _ := json.Marshal(map[string]string{
88
+ "app_id": os.Getenv("KOKO_APP_ID"), "subject": hex.EncodeToString(mac.Sum(nil)),
89
+ })
90
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel()
91
+ req, _ := http.NewRequestWithContext(ctx, http.MethodPost, kokoGateway+"/v1/sdk/token", bytes.NewReader(body))
92
+ req.SetBasicAuth(os.Getenv("KOKO_CLIENT_ID"), os.Getenv("KOKO_CLIENT_SECRET"))
93
+ req.Header.Set("Content-Type", "application/json")
94
+ resp, err := http.DefaultClient.Do(req); if err != nil { return "", 0, err }
95
+ defer resp.Body.Close()
96
+ if resp.StatusCode != http.StatusOK { return "", 0, fmt.Errorf("koko token: %s", resp.Status) }
97
+ var out struct { AccessToken string `json:"access_token"`; ExpiresAt int64 `json:"expires_at"` }
98
+ if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", 0, err }
99
+ return out.AccessToken, out.ExpiresAt * 1000, nil
100
+ }
101
+ ```
102
+
103
+ ## Python(FastAPI + HTTPX)
104
+
105
+ ```python
106
+ import hashlib, hmac, os
107
+ import httpx
108
+ from fastapi import Depends, HTTPException, Response
109
+
110
+ @app.get('/koko/access-token')
111
+ async def koko_token(response: Response, user=Depends(require_host_user)):
112
+ subject = hmac.new(os.environ['KOKO_SUBJECT_SALT'].encode(), str(user.id).encode(), hashlib.sha256).hexdigest()
113
+ async with httpx.AsyncClient(timeout=5.0) as client:
114
+ upstream = await client.post(
115
+ f'{KOKO_GATEWAY}/v1/sdk/token',
116
+ auth=(os.environ['KOKO_CLIENT_ID'], os.environ['KOKO_CLIENT_SECRET']),
117
+ json={'app_id': os.environ['KOKO_APP_ID'], 'subject': subject},
118
+ )
119
+ if upstream.status_code != 200:
120
+ raise HTTPException(502, 'Koko token unavailable')
121
+ token = upstream.json()
122
+ response.headers['Cache-Control'] = 'no-store'
123
+ return {'accessToken': token['access_token'], 'expiresAt': token['expires_at'] * 1000}
124
+ ```
125
+
126
+ ## PHP(Laravel)
127
+
128
+ ```php
129
+ public function token(Request $request): JsonResponse
130
+ {
131
+ $userId = (string) $request->user()->getAuthIdentifier(); // auth middleware 已验证
132
+ $subject = hash_hmac('sha256', $userId, env('KOKO_SUBJECT_SALT'));
133
+ $upstream = Http::timeout(5)
134
+ ->withBasicAuth(env('KOKO_CLIENT_ID'), env('KOKO_CLIENT_SECRET'))
135
+ ->post(config('services.koko.gateway').'/v1/sdk/token', [
136
+ 'app_id' => env('KOKO_APP_ID'), 'subject' => $subject,
137
+ ]);
138
+ abort_unless($upstream->successful(), 502, 'Koko token unavailable');
139
+ return response()->json([
140
+ 'accessToken' => $upstream['access_token'],
141
+ 'expiresAt' => $upstream['expires_at'] * 1000,
142
+ ])->header('Cache-Control', 'no-store');
143
+ }
144
+ ```
145
+
146
+ ## C#(ASP.NET Core)
147
+
148
+ ```csharp
149
+ app.MapGet("/koko/access-token", async (ClaimsPrincipal user, IHttpClientFactory factory) => {
150
+ var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)
151
+ ?? throw new UnauthorizedAccessException();
152
+ var subject = Convert.ToHexString(HMACSHA256.HashData(
153
+ Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("KOKO_SUBJECT_SALT")!),
154
+ Encoding.UTF8.GetBytes(userId))).ToLowerInvariant();
155
+ var clientId = Environment.GetEnvironmentVariable("KOKO_CLIENT_ID")!;
156
+ var clientSecret = Environment.GetEnvironmentVariable("KOKO_CLIENT_SECRET")!;
157
+ var appId = Environment.GetEnvironmentVariable("KOKO_APP_ID")!;
158
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{kokoGateway}/v1/sdk/token");
159
+ var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(
160
+ $"{clientId}:{clientSecret}"));
161
+ request.Headers.Authorization = new("Basic", credentials);
162
+ request.Content = JsonContent.Create(new { app_id = appId, subject });
163
+ var upstream = await factory.CreateClient("koko").SendAsync(request);
164
+ if (!upstream.IsSuccessStatusCode) return Results.Problem(statusCode: 502, title: "Koko token unavailable");
165
+ var token = await upstream.Content.ReadFromJsonAsync<KokoToken>();
166
+ return Results.Json(new { accessToken = token!.access_token, expiresAt = token.expires_at * 1000 },
167
+ headers: new Dictionary<string, string> { ["Cache-Control"] = "no-store" });
168
+ }).RequireAuthorization();
169
+ ```
170
+
171
+ ## App 端
172
+
173
+ ```tsx
174
+ const tokenProvider = React.useCallback(async () => {
175
+ const response = await hostApi.get('/koko/access-token');
176
+ return response.data as { accessToken: string; expiresAt: number };
177
+ }, []);
178
+
179
+ <KokoProvider config={{
180
+ gateway: {
181
+ baseUrl: 'https://sdk.intentcomputing.cn',
182
+ tokenProvider,
183
+ },
184
+ }}>{children}</KokoProvider>
185
+ ```
186
+
187
+ 不要把 token 写入 AsyncStorage、日志或分析事件。SDK 自己在内存中缓存,到期前刷新;如果网关返回 401,会清除缓存并强制调用一次 `tokenProvider`。
@@ -8,14 +8,14 @@ For React Native host apps. After installing the package you must link native co
8
8
 
9
9
  - React Native ≥ 0.73 (or Expo Dev Client / `expo prebuild` — **not Expo Go**)
10
10
  - iOS deployment target ≥ 15; ability to run `pod install`
11
- - An **app token** from Intent Computing (`gateway.token` — not a DeepSeek / OpenAI key)
11
+ - An Intent Computing `clientId` and `clientSecret`; keep the secret on the host backend only
12
12
 
13
13
  ## Checklist
14
14
 
15
15
  1. Install the npm package
16
16
  2. iOS: `pod install` (Android usually autolinks)
17
17
  3. **Rebuild native** (Debug or Release)
18
- 4. Wrap the root tree with `KokoProvider` and set `token`
18
+ 4. Exchange credentials on the host backend and provide short-lived tokens to `KokoProvider`
19
19
  5. Launch and send a natural-language goal
20
20
 
21
21
  JS-only config changes can hot-reload via Metro. **First integration or any native SDK change requires a native rebuild.**
@@ -65,11 +65,15 @@ Wrap at the app root (or outermost navigation shell):
65
65
  import { KokoProvider } from '@intentcomputing/koko-agent-sdk';
66
66
 
67
67
  export default function App() {
68
+ const tokenProvider = React.useCallback(async () => {
69
+ const response = await hostApi.get('/koko/access-token');
70
+ return response.data as { accessToken: string; expiresAt: number };
71
+ }, []);
68
72
  return (
69
73
  <KokoProvider
70
74
  config={{
71
75
  gateway: {
72
- token: 'YOUR_APP_TOKEN',
76
+ tokenProvider,
73
77
  // Default: https://sdk.intentcomputing.net
74
78
  // China: https://sdk.intentcomputing.cn
75
79
  // baseUrl: 'https://sdk.intentcomputing.cn',
@@ -85,9 +89,11 @@ export default function App() {
85
89
  }
86
90
  ```
87
91
 
92
+ `tokenProvider` calls your backend, never Koko's exchange endpoint directly: the `clientSecret` must not enter the app. The SDK caches credentials using `expiresAt` or JWT `exp` and refreshes once after a 401. See the [backend demos](BACKEND_AUTH_DEMOS.md) for complete examples. Static `gateway.token` remains available only for local development and migration.
93
+
88
94
  `KokoProvider` will:
89
95
 
90
- - Initialize the agent with `gateway.token`
96
+ - Initialize the agent with `gateway.tokenProvider`
91
97
  - Mount cursor, progress, and chat sheet (optional — see below)
92
98
 
93
99
  Open chat from host UI:
@@ -138,7 +144,7 @@ For custom private UIs, declare the gate before navigation or reveal and keep it
138
144
 
139
145
  1. Launch the app and use your host button wired to `openKokoChat()`. The provider does not create a host entry button.
140
146
  2. Send a verifiable goal; you should see dump → tap with a visible cursor.
141
- 3. Check that requests use the selected official gateway and a token bound to this app. Do not bundle an LLM API key. Validate privacy gates and outbound payloads using synthetic data.
147
+ 3. Check that requests use the selected official gateway and a short-lived token bound to this app. Do not bundle a client secret, long-lived token or LLM API key. Validate privacy gates and outbound payloads using synthetic data.
142
148
 
143
149
  ---
144
150
 
@@ -157,7 +163,7 @@ Not supported. Use a Dev Client, bare RN, or a production host binary.
157
163
  Use `portal:` / `file:`. If the package lives **outside** the monorepo, Metro may need `watchFolders` + `extraNodeModules` (usually unnecessary after npm publish).
158
164
 
159
165
  **Where does the token come from?**
160
- Intent Computing console / commercial onboarding — not a model-vendor API key.
166
+ Your backend exchanges its server-only Intent Computing client credentials for a short-lived token. It is not a model-vendor API key.
161
167
 
162
168
  ---
163
169
 
@@ -166,3 +172,4 @@ Intent Computing console / commercial onboarding — not a model-vendor API key.
166
172
  - Package overview: [../README.md](../README.md)
167
173
  - npm publish: [PUBLISH.md](./PUBLISH.md)
168
174
  - 中文接入指南: [INTEGRATION.zh-CN.md](./INTEGRATION.zh-CN.md)
175
+ - Backend authentication demos: [BACKEND_AUTH_DEMOS.md](./BACKEND_AUTH_DEMOS.md)
@@ -8,14 +8,14 @@
8
8
 
9
9
  - React Native ≥ 0.73(或 Expo Dev Client / `expo prebuild`,**不支持 Expo Go**)
10
10
  - iOS 部署目标 ≥ 15;需本机可 `pod install`
11
- - 从 Intent Computing 拿到 **应用 token**(`gateway.token`,不是 DeepSeek / OpenAI Key)
11
+ - 从 Intent Computing 拿到 `clientId` / `clientSecret`;`clientSecret` 只放宿主后端
12
12
 
13
13
  ## 步骤一览
14
14
 
15
15
  1. 安装 npm 包
16
16
  2. iOS `pod install`(Android 一般靠 autolinking)
17
17
  3. **重新编译原生**(Debug / Release 均可)
18
- 4. 根组件包一层 `KokoProvider` 并配置 token
18
+ 4. 宿主后端换取短期 token,App 用 `tokenProvider` 提供给 `KokoProvider`
19
19
  5. 将 `openKokoChat()` 对接宿主入口,再发自然语言任务
20
20
 
21
21
  之后若只改 JS 配置或升级包的纯 JS 部分,Metro 热更新即可;**SDK 含原生改动或首次接入,必须再编原生。**
@@ -63,11 +63,15 @@ npx react-native run-android
63
63
  import { KokoProvider } from '@intentcomputing/koko-agent-sdk';
64
64
 
65
65
  export default function App() {
66
+ const tokenProvider = React.useCallback(async () => {
67
+ const response = await hostApi.get('/koko/access-token');
68
+ return response.data as { accessToken: string; expiresAt: number };
69
+ }, []);
66
70
  return (
67
71
  <KokoProvider
68
72
  config={{
69
73
  gateway: {
70
- token: 'YOUR_APP_TOKEN',
74
+ tokenProvider,
71
75
  // 默认 https://sdk.intentcomputing.net(OpenAI)
72
76
  // 国内:https://sdk.intentcomputing.cn(DeepSeek)
73
77
  baseUrl: 'https://sdk.intentcomputing.cn',
@@ -82,6 +86,8 @@ export default function App() {
82
86
  }
83
87
  ```
84
88
 
89
+ `tokenProvider` 应调用宿主后端,不能直接调用 Koko 换 token 接口,因为 `clientSecret` 绝不能进入 App。SDK 会按 `expiresAt` 或 JWT `exp` 缓存凭证,并在 401 后刷新一次。宿主后端完整实现见[六种主流语言 Demo](BACKEND_AUTH_DEMOS.zh-CN.md)。`gateway.token` 仅保留给本地开发和迁移,不建议用于正式包。
90
+
85
91
  宿主也可主动打开会话:
86
92
 
87
93
  ```tsx
@@ -113,7 +119,7 @@ getKokoAgent().openChat();
113
119
 
114
120
  1. 启动 App,通过接有 `openKokoChat()` 的宿主按钮打开助手;Provider 不会自动创建宿主入口。
115
121
  2. 输入可核验目标,应看到 dump → tap 与光标。
116
- 3. 请求打到官网网关(或 `baseUrl`),App 内无任何 LLM API Key。
122
+ 3. 请求打到官网网关(或 `baseUrl`),App 内无 `clientSecret`、长期 token 或任何 LLM API Key。
117
123
 
118
124
  ---
119
125
 
@@ -138,3 +144,4 @@ getKokoAgent().openChat();
138
144
  - 英文总览:[../README.md](../README.md)
139
145
  - 中文总览:[../README.zh-CN.md](../README.zh-CN.md)
140
146
  - 发布:[PUBLISH.zh-CN.md](./PUBLISH.zh-CN.md)
147
+ - 后端鉴权 Demo:[BACKEND_AUTH_DEMOS.zh-CN.md](./BACKEND_AUTH_DEMOS.zh-CN.md)
@@ -29,8 +29,7 @@ var _ExecutionGuard = require("./commercial/ExecutionGuard.js");
29
29
  var _AgentDebug = require("./commercial/AgentDebug.js");
30
30
  var _index = require("./i18n/index.js");
31
31
  var _ocrTarget = require("./assist/ocrTarget.js");
32
- var cache;
33
- function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
32
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
34
33
  /**
35
34
  * Koko Agent SDK runtime — dump / find / tap / swipe / pinch / type + cursor + confirm gate + task log.
36
35
  */
@@ -173,14 +172,18 @@ class KokoAgent {
173
172
  if (this.activeSession || this.claimingCheckpoint) {
174
173
  throw new Error((0, _index.runtimeText)('runtime.alreadyRunning', undefined, this.config?.locale));
175
174
  }
176
- const token = opts?.token ?? this.config.gateway?.token;
177
- if (!token) {
175
+ const gateway = {
176
+ token: opts?.token ?? this.config.gateway?.token,
177
+ tokenProvider: opts?.tokenProvider ?? this.config.gateway?.tokenProvider,
178
+ appId: opts?.appId ?? this.config.gateway?.appId
179
+ };
180
+ if (!gateway.token && !gateway.tokenProvider) {
178
181
  throw new Error(this.text('runtime.missingToken'));
179
182
  }
180
183
  const baseUrl = opts?.baseUrl ?? this.config.gateway?.baseUrl;
181
184
  const locale = (0, _index.gatewayLocale)((0, _index.currentLocale)(opts?.locale ?? this.config.locale));
182
185
  void _AccountStore.AccountStore.refresh({
183
- token,
186
+ ...gateway,
184
187
  baseUrl
185
188
  });
186
189
  const session = this.createSession({
@@ -191,7 +194,7 @@ class KokoAgent {
191
194
  onAskUser: opts?.onAskUser,
192
195
  onNarration: opts?.onNarration,
193
196
  baseUrl,
194
- token
197
+ ...gateway
195
198
  });
196
199
  this.activeSession = session;
197
200
  // An action can take down the context running it (locale/theme/RTL change,
@@ -206,7 +209,7 @@ class KokoAgent {
206
209
  await _RunCheckpoint.RunCheckpoint.clear().catch(() => {});
207
210
  if (this.activeSession === session) this.activeSession = null;
208
211
  void _AccountStore.AccountStore.refresh({
209
- token,
212
+ ...gateway,
210
213
  baseUrl
211
214
  });
212
215
  }
@@ -263,11 +266,10 @@ class KokoAgent {
263
266
 
264
267
  /** Refresh plan / quota / entitlements from gateway. */
265
268
  async refreshAccount() {
266
- const token = this.config.gateway?.token;
267
- if (!token) return null;
269
+ const gateway = this.config.gateway;
270
+ if (!gateway?.token && !gateway?.tokenProvider) return null;
268
271
  return _AccountStore.AccountStore.refresh({
269
- token,
270
- baseUrl: this.config.gateway?.baseUrl
272
+ ...gateway
271
273
  });
272
274
  }
273
275
  getAccount() {