@chatu-ai/app-sdk 0.2.1
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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +25 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/kv.d.ts +23 -0
- package/dist/kv.js +84 -0
- package/dist/kv.test.d.ts +1 -0
- package/dist/kv.test.js +41 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chatu-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @chatu-ai/app-sdk
|
|
2
|
+
|
|
3
|
+
Data SDK for apps generated by **ChatU Builder**. One API, driver picked from environment variables — works in the Builder preview, on your own server, or with no config at all.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { kv } from '@chatu-ai/app-sdk' // server-side only (Route Handlers / Server Components / Server Actions)
|
|
7
|
+
|
|
8
|
+
await kv.set('todos:1', { title: 'hi' }, { ex: 3600 })
|
|
9
|
+
const todo = await kv.get<{ title: string }>('todos:1')
|
|
10
|
+
await kv.incr('views')
|
|
11
|
+
const { keys } = await kv.list('todos:')
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Env | Driver | Notes |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `CHATU_DATA_URL` + `CHATU_APP_KEY` (+ `CHATU_DATA_ENV=dev\|prod`) | **platform** — ChatU hosted Data API, metered | set automatically in the Builder preview; copy from the publish panel for your own server |
|
|
17
|
+
| none | **memory** — in-process, lost on restart | local dev / fallback |
|
|
18
|
+
|
|
19
|
+
Never expose `CHATU_APP_KEY` to the browser. MIT.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 驱动选择(技术方案 15 §1):
|
|
3
|
+
* - CHATU_DATA_URL + CHATU_APP_KEY(或 CHATU_CUSTOMER_API_KEY)→ platform(平台托管 Data API,开发期/线上都可用,按用量计费)
|
|
4
|
+
* - 都没有 → memory(进程内存,重启即丢;本地开发/无配置降级)
|
|
5
|
+
* 只在服务端使用(Route Handler / Server Component / Server Action);密钥不得暴露给浏览器。
|
|
6
|
+
*/
|
|
7
|
+
export type DriverKind = 'platform' | 'memory';
|
|
8
|
+
export interface PlatformConfig {
|
|
9
|
+
kind: 'platform';
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
env: 'dev' | 'prod';
|
|
13
|
+
fetchImpl: typeof fetch;
|
|
14
|
+
}
|
|
15
|
+
export interface MemoryConfig {
|
|
16
|
+
kind: 'memory';
|
|
17
|
+
}
|
|
18
|
+
export type ResolvedConfig = PlatformConfig | MemoryConfig;
|
|
19
|
+
export interface ConfigureOptions {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
env?: 'dev' | 'prod';
|
|
23
|
+
driver?: DriverKind;
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
/** 显式配置(测试或非 env 场景);不调用则完全由环境变量决定 */
|
|
27
|
+
export declare function configure(options: ConfigureOptions): void;
|
|
28
|
+
export declare function resolveConfig(): ResolvedConfig;
|
|
29
|
+
/** 当前生效的驱动与环境(诊断用,不含密钥) */
|
|
30
|
+
export declare function describe(): {
|
|
31
|
+
driver: DriverKind;
|
|
32
|
+
env?: 'dev' | 'prod';
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let override = {};
|
|
2
|
+
/** 显式配置(测试或非 env 场景);不调用则完全由环境变量决定 */
|
|
3
|
+
export function configure(options) {
|
|
4
|
+
override = { ...options };
|
|
5
|
+
}
|
|
6
|
+
export function resolveConfig() {
|
|
7
|
+
// 不依赖 @types/node:通过 globalThis 读取 process.env
|
|
8
|
+
const proc = globalThis.process;
|
|
9
|
+
const env = proc?.env ?? {};
|
|
10
|
+
const baseUrl = override.baseUrl ?? env.CHATU_DATA_URL;
|
|
11
|
+
const apiKey = override.apiKey ?? env.CHATU_APP_KEY ?? env.CHATU_CUSTOMER_API_KEY;
|
|
12
|
+
const driver = override.driver ?? (baseUrl && apiKey ? 'platform' : 'memory');
|
|
13
|
+
if (driver === 'platform') {
|
|
14
|
+
if (!baseUrl || !apiKey)
|
|
15
|
+
throw new Error('@chatu-ai/app-sdk: platform driver requires CHATU_DATA_URL and CHATU_APP_KEY');
|
|
16
|
+
const dataEnv = (override.env ?? env.CHATU_DATA_ENV ?? 'dev').toLowerCase() === 'prod' ? 'prod' : 'dev';
|
|
17
|
+
return { kind: 'platform', baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, env: dataEnv, fetchImpl: override.fetchImpl ?? fetch };
|
|
18
|
+
}
|
|
19
|
+
return { kind: 'memory' };
|
|
20
|
+
}
|
|
21
|
+
/** 当前生效的驱动与环境(诊断用,不含密钥) */
|
|
22
|
+
export function describe() {
|
|
23
|
+
const c = resolveConfig();
|
|
24
|
+
return c.kind === 'platform' ? { driver: 'platform', env: c.env, baseUrl: c.baseUrl } : { driver: 'memory' };
|
|
25
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { kv, getKv } from './kv.js';
|
|
2
|
+
export type { KvClient, KvSetOptions, KvListResult } from './kv.js';
|
|
3
|
+
export { configure, describe } from './config.js';
|
|
4
|
+
export type { ConfigureOptions, DriverKind } from './config.js';
|
|
5
|
+
export { AppSdkError } from './errors.js';
|
package/dist/index.js
ADDED
package/dist/kv.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface KvSetOptions {
|
|
2
|
+
ex?: number;
|
|
3
|
+
}
|
|
4
|
+
export interface KvListResult {
|
|
5
|
+
keys: string[];
|
|
6
|
+
nextCursor: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface KvClient {
|
|
9
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
10
|
+
set(key: string, value: unknown, opts?: KvSetOptions): Promise<void>;
|
|
11
|
+
del(key: string): Promise<boolean>;
|
|
12
|
+
incr(key: string, by?: number): Promise<number>;
|
|
13
|
+
expire(key: string, seconds: number): Promise<boolean>;
|
|
14
|
+
mget<T = unknown>(keys: string[]): Promise<Array<T | null>>;
|
|
15
|
+
list(prefix?: string, opts?: {
|
|
16
|
+
cursor?: string | null;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}): Promise<KvListResult>;
|
|
19
|
+
}
|
|
20
|
+
/** 按当前配置取 KV 客户端(惰性、缓存;configure() 后自动重建) */
|
|
21
|
+
export declare function getKv(): KvClient;
|
|
22
|
+
/** 便捷单例:`import { kv } from '@chatu-ai/app-sdk'` */
|
|
23
|
+
export declare const kv: KvClient;
|
package/dist/kv.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { resolveConfig } from './config.js';
|
|
2
|
+
import { AppSdkError } from './errors.js';
|
|
3
|
+
// ---------- platform driver ----------
|
|
4
|
+
function platformKv(cfg) {
|
|
5
|
+
const headers = { 'x-api-key': cfg.apiKey, 'x-chatu-env': cfg.env, 'content-type': 'application/json' };
|
|
6
|
+
async function call(method, path, body) {
|
|
7
|
+
const res = await cfg.fetchImpl(`${cfg.baseUrl}/kv${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
8
|
+
let json = null;
|
|
9
|
+
try {
|
|
10
|
+
json = await res.json();
|
|
11
|
+
}
|
|
12
|
+
catch { /* ignore */ }
|
|
13
|
+
if (!res.ok || json?.ok === false) {
|
|
14
|
+
throw new AppSdkError(json?.error ?? `HTTP_${res.status}`, json?.message ?? `kv ${method} ${path} failed (${res.status})`, res.status);
|
|
15
|
+
}
|
|
16
|
+
return json;
|
|
17
|
+
}
|
|
18
|
+
const enc = (key) => key.split('/').map(encodeURIComponent).join('/');
|
|
19
|
+
return {
|
|
20
|
+
async get(key) { const r = await call('GET', `/${enc(key)}`); return r.exists ? r.value : null; },
|
|
21
|
+
async set(key, value, opts) { await call('PUT', `/${enc(key)}`, { value, ex: opts?.ex }); },
|
|
22
|
+
async del(key) { const r = await call('DELETE', `/${enc(key)}`); return r.removed; },
|
|
23
|
+
async incr(key, by = 1) { const r = await call('POST', '/incr', { key, by }); return r.value; },
|
|
24
|
+
async expire(key, seconds) { const r = await call('POST', '/expire', { key, seconds }); return r.applied; },
|
|
25
|
+
async mget(keys) { const r = await call('POST', '/mget', { keys }); return r.items.map(i => (i.exists ? i.value : null)); },
|
|
26
|
+
async list(prefix = '', opts) {
|
|
27
|
+
const q = new URLSearchParams({ prefix, limit: String(opts?.limit ?? 100) });
|
|
28
|
+
if (opts?.cursor)
|
|
29
|
+
q.set('cursor', opts.cursor);
|
|
30
|
+
const r = await call('GET', `?${q.toString()}`);
|
|
31
|
+
return { keys: r.keys, nextCursor: r.nextCursor ?? null };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ---------- memory driver ----------
|
|
36
|
+
function memoryKv() {
|
|
37
|
+
const store = new Map();
|
|
38
|
+
const live = (key) => {
|
|
39
|
+
const e = store.get(key);
|
|
40
|
+
if (!e)
|
|
41
|
+
return null;
|
|
42
|
+
if (e.expiresAt !== undefined && Date.now() > e.expiresAt) {
|
|
43
|
+
store.delete(key);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return e;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
async get(key) { return live(key)?.value ?? null; },
|
|
50
|
+
async set(key, value, opts) { store.set(key, { value, expiresAt: opts?.ex ? Date.now() + opts.ex * 1000 : undefined }); },
|
|
51
|
+
async del(key) { return store.delete(key); },
|
|
52
|
+
async incr(key, by = 1) { const cur = Number(live(key)?.value ?? 0); if (!Number.isInteger(cur))
|
|
53
|
+
throw new AppSdkError('NOT_AN_INTEGER', 'value is not an integer'); const next = cur + by; store.set(key, { value: next }); return next; },
|
|
54
|
+
async expire(key, seconds) { const e = live(key); if (!e)
|
|
55
|
+
return false; e.expiresAt = Date.now() + seconds * 1000; return true; },
|
|
56
|
+
async mget(keys) { return keys.map(k => live(k)?.value ?? null); },
|
|
57
|
+
async list(prefix = '', opts) {
|
|
58
|
+
const all = [...store.keys()].filter(k => k.startsWith(prefix) && live(k)).sort();
|
|
59
|
+
const start = opts?.cursor ? Number(opts.cursor) : 0;
|
|
60
|
+
const limit = opts?.limit ?? 100;
|
|
61
|
+
const page = all.slice(start, start + limit);
|
|
62
|
+
return { keys: page, nextCursor: start + limit < all.length ? String(start + limit) : null };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let cached = null;
|
|
67
|
+
/** 按当前配置取 KV 客户端(惰性、缓存;configure() 后自动重建) */
|
|
68
|
+
export function getKv() {
|
|
69
|
+
const cfg = resolveConfig();
|
|
70
|
+
const key = cfg.kind === 'platform' ? `platform|${cfg.baseUrl}|${cfg.env}|${cfg.apiKey.slice(-4)}` : 'memory';
|
|
71
|
+
if (!cached || cached.key !== key)
|
|
72
|
+
cached = { key, client: cfg.kind === 'platform' ? platformKv(cfg) : memoryKv() };
|
|
73
|
+
return cached.client;
|
|
74
|
+
}
|
|
75
|
+
/** 便捷单例:`import { kv } from '@chatu-ai/app-sdk'` */
|
|
76
|
+
export const kv = {
|
|
77
|
+
get: (k) => getKv().get(k),
|
|
78
|
+
set: (k, v, o) => getKv().set(k, v, o),
|
|
79
|
+
del: (k) => getKv().del(k),
|
|
80
|
+
incr: (k, b) => getKv().incr(k, b),
|
|
81
|
+
expire: (k, s) => getKv().expire(k, s),
|
|
82
|
+
mget: (ks) => getKv().mget(ks),
|
|
83
|
+
list: (p, o) => getKv().list(p, o),
|
|
84
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/kv.test.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { beforeEach, describe as d, expect, it } from 'vitest';
|
|
2
|
+
import { configure, describe, getKv, kv } from './index';
|
|
3
|
+
d('memory driver', () => {
|
|
4
|
+
beforeEach(() => configure({ driver: 'memory' }));
|
|
5
|
+
it('round-trips, ttl, incr, list', async () => {
|
|
6
|
+
expect(describe().driver).toBe('memory');
|
|
7
|
+
await kv.set('todos:1', { a: 1 });
|
|
8
|
+
expect(await kv.get('todos:1')).toEqual({ a: 1 });
|
|
9
|
+
expect(await kv.incr('views')).toBe(1);
|
|
10
|
+
expect(await kv.incr('views', 5)).toBe(6);
|
|
11
|
+
await kv.set('todos:2', 'x', { ex: 1 });
|
|
12
|
+
expect((await kv.list('todos:')).keys).toEqual(['todos:1', 'todos:2']);
|
|
13
|
+
expect(await kv.mget(['todos:1', 'nope'])).toEqual([{ a: 1 }, null]);
|
|
14
|
+
expect(await kv.del('todos:1')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
d('platform driver', () => {
|
|
18
|
+
it('sends x-api-key / x-chatu-env and unwraps responses', async () => {
|
|
19
|
+
const calls = [];
|
|
20
|
+
const fetchImpl = (async (url, init) => {
|
|
21
|
+
calls.push({ url, init });
|
|
22
|
+
if (init.method === 'GET' && url.endsWith('/kv/todos%3A1'))
|
|
23
|
+
return new Response(JSON.stringify({ ok: true, exists: true, value: { a: 1 } }), { status: 200 });
|
|
24
|
+
if (init.method === 'PUT')
|
|
25
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
26
|
+
if (url.endsWith('/kv/incr'))
|
|
27
|
+
return new Response(JSON.stringify({ ok: true, value: 7 }), { status: 200 });
|
|
28
|
+
return new Response(JSON.stringify({ ok: false, error: 'INVALID_KEY' }), { status: 400 });
|
|
29
|
+
});
|
|
30
|
+
configure({ driver: 'platform', baseUrl: 'https://api.test/data/v1/', apiKey: 'sk-conv-abc', env: 'prod', fetchImpl });
|
|
31
|
+
expect(describe()).toEqual({ driver: 'platform', env: 'prod', baseUrl: 'https://api.test/data/v1' });
|
|
32
|
+
await getKv().set('todos:1', { a: 1 }, { ex: 60 });
|
|
33
|
+
expect(await kv.get('todos:1')).toEqual({ a: 1 });
|
|
34
|
+
expect(await kv.incr('views', 7)).toBe(7);
|
|
35
|
+
const h = calls[0].init.headers;
|
|
36
|
+
expect(h['x-api-key']).toBe('sk-conv-abc');
|
|
37
|
+
expect(h['x-chatu-env']).toBe('prod');
|
|
38
|
+
expect(JSON.parse(String(calls[0].init.body))).toEqual({ value: { a: 1 }, ex: 60 });
|
|
39
|
+
await expect(kv.get('bad*')).rejects.toMatchObject({ code: 'INVALID_KEY', status: 400 });
|
|
40
|
+
});
|
|
41
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chatu-ai/app-sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Runtime data SDK for apps generated by ChatU Builder: kv (and more) with platform / memory drivers selected by environment variables",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/chatu-ai/chatu-builder-sdk.git",
|
|
23
|
+
"directory": "packages/app-sdk"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/chatu-ai/chatu-builder-sdk/tree/main/packages/app-sdk#readme",
|
|
26
|
+
"bugs": "https://github.com/chatu-ai/chatu-builder-sdk/issues",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"chatu",
|
|
29
|
+
"builder",
|
|
30
|
+
"kv",
|
|
31
|
+
"storage",
|
|
32
|
+
"app-sdk",
|
|
33
|
+
"nextjs"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.7.0",
|
|
44
|
+
"vitest": "^3.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|