@x-otto/remote-persistence-server 0.0.1-alpha.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.
- package/README.md +193 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @x-otto/remote-persistence-server
|
|
2
|
+
|
|
3
|
+
独立部署的 Otto Remote 持久化服务端 — Hono + SQLite,6 个路由组(session 存储 + trace + auth +
|
|
4
|
+
config + memory + memory-workspace),Bearer token 认证。
|
|
5
|
+
|
|
6
|
+
## 快速开始
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# 安装
|
|
10
|
+
pnpm install
|
|
11
|
+
|
|
12
|
+
# 启动(开发环境,无需 token)
|
|
13
|
+
DATA_DIR=./data npx tsx src/index.ts
|
|
14
|
+
|
|
15
|
+
# 启动(生产环境,需要 token)
|
|
16
|
+
AUTH_TOKEN=my-secret-token DATA_DIR=/var/otto-storage PORT=3001 npx tsx src/index.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 环境变量
|
|
20
|
+
|
|
21
|
+
| 变量 | 必填 | 默认值 | 说明 |
|
|
22
|
+
|------|------|--------|------|
|
|
23
|
+
| `DATA_DIR` | 否 | `process.cwd()` | SQLite 数据库文件目录 |
|
|
24
|
+
| `PORT` | 否 | `3001` | 监听端口 |
|
|
25
|
+
| `AUTH_TOKEN` | **生产必填** | `''`(空=不启用认证) | Bearer token |
|
|
26
|
+
|
|
27
|
+
> ⚠️ 不设置 `AUTH_TOKEN` 时服务**不启用认证**,仅限开发环境使用。
|
|
28
|
+
|
|
29
|
+
## API 参考
|
|
30
|
+
|
|
31
|
+
### 健康检查
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
GET /health
|
|
35
|
+
→ 200 { "ok": true }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
无需认证。
|
|
39
|
+
|
|
40
|
+
### 会话快照 CRUD
|
|
41
|
+
|
|
42
|
+
所有 `/sessions/*` 路径需要 `Authorization: Bearer <AUTH_TOKEN>` header。
|
|
43
|
+
|
|
44
|
+
#### 列出所有会话 ID
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
GET /sessions
|
|
48
|
+
Authorization: Bearer <token>
|
|
49
|
+
→ 200 { "ids": ["session-1", "session-2"] }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
按 `updated_at` 降序排列。
|
|
53
|
+
|
|
54
|
+
#### 获取单个快照
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
GET /sessions/:id
|
|
58
|
+
Authorization: Bearer <token>
|
|
59
|
+
→ 200 <Snapshot JSON>
|
|
60
|
+
→ 404 { "error": "not_found" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
返回客户端 `PUT` 时的完整 JSON。
|
|
64
|
+
|
|
65
|
+
#### 保存/更新快照
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
PUT /sessions/:id
|
|
69
|
+
Authorization: Bearer <token>
|
|
70
|
+
Content-Type: application/json
|
|
71
|
+
|
|
72
|
+
<Snapshot JSON>
|
|
73
|
+
→ 200 { "ok": true }
|
|
74
|
+
→ 400 { "error": "invalid_body" }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
幂等操作,重复 `PUT` 会用新数据覆盖。
|
|
78
|
+
|
|
79
|
+
#### 删除快照
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
DELETE /sessions/:id
|
|
83
|
+
Authorization: Bearer <token>
|
|
84
|
+
→ 200 { "ok": true }
|
|
85
|
+
→ 404 { "error": "not_found" }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 认证错误
|
|
89
|
+
|
|
90
|
+
| 场景 | 状态码 | Body |
|
|
91
|
+
|------|--------|------|
|
|
92
|
+
| 无 `Authorization` header | 401 | `{ "error": "unauthorized" }` |
|
|
93
|
+
| Token 不匹配或非 Bearer 类型 | 403 | `{ "error": "forbidden" }` |
|
|
94
|
+
|
|
95
|
+
Token 比较使用 `crypto.timingSafeEqual`,防止时序攻击。
|
|
96
|
+
|
|
97
|
+
## 客户端接入(Otto 侧)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { createRemoteSessionPool } from '@x-otto/runtime'
|
|
101
|
+
|
|
102
|
+
const pool = createRemoteSessionPool({
|
|
103
|
+
sessionUrl: 'http://your-server:3001/sessions',
|
|
104
|
+
fetch: globalThis.fetch,
|
|
105
|
+
getAuth: async () => ({ token: 'my-secret-token' }),
|
|
106
|
+
hookRegistry,
|
|
107
|
+
providerRegistry,
|
|
108
|
+
logger,
|
|
109
|
+
configProvider,
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
或通过 `AppOptions` 声明式配置:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const app = createApp({
|
|
117
|
+
storage: {
|
|
118
|
+
session: { url: 'http://your-server:3001/sessions' },
|
|
119
|
+
remote: {
|
|
120
|
+
getAuth: async () => ({ token: 'my-secret-token' }),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Docker 部署
|
|
127
|
+
|
|
128
|
+
```dockerfile
|
|
129
|
+
FROM node:22-alpine
|
|
130
|
+
|
|
131
|
+
WORKDIR /app
|
|
132
|
+
|
|
133
|
+
# 安装依赖(better-sqlite3 需要编译工具)
|
|
134
|
+
RUN apk add --no-cache python3 make g++
|
|
135
|
+
|
|
136
|
+
# 注意:pnpm-lock.yaml 位于 monorepo 根目录,非本包目录——构建上下文需设为仓库根
|
|
137
|
+
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
|
138
|
+
COPY packages/remote-persistence-server/package.json packages/remote-persistence-server/
|
|
139
|
+
RUN corepack enable && pnpm install --prod --filter @x-otto/remote-persistence-server
|
|
140
|
+
|
|
141
|
+
COPY dist/ ./dist/
|
|
142
|
+
|
|
143
|
+
ENV DATA_DIR=/data
|
|
144
|
+
ENV PORT=3001
|
|
145
|
+
|
|
146
|
+
VOLUME /data
|
|
147
|
+
|
|
148
|
+
EXPOSE 3001
|
|
149
|
+
|
|
150
|
+
CMD ["node", "dist/index.js"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
docker build -t otto-persistence-server .
|
|
155
|
+
docker run -d \
|
|
156
|
+
-v /host/data:/data \
|
|
157
|
+
-e AUTH_TOKEN=my-secret-token \
|
|
158
|
+
-p 3001:3001 \
|
|
159
|
+
otto-persistence-server
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 开发
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# 类型检查
|
|
166
|
+
pnpm typecheck
|
|
167
|
+
|
|
168
|
+
# 构建
|
|
169
|
+
pnpm build
|
|
170
|
+
|
|
171
|
+
# 运行测试
|
|
172
|
+
pnpm test
|
|
173
|
+
|
|
174
|
+
# 监听模式
|
|
175
|
+
pnpm test:watch
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 存储
|
|
179
|
+
|
|
180
|
+
SQLite 数据库文件位于 `<DATA_DIR>/otto.db`,WAL 模式,表结构:
|
|
181
|
+
|
|
182
|
+
```sql
|
|
183
|
+
CREATE TABLE snapshots (
|
|
184
|
+
id TEXT PRIMARY KEY,
|
|
185
|
+
data TEXT NOT NULL, -- 完整 Snapshot JSON
|
|
186
|
+
created_at INTEGER NOT NULL,
|
|
187
|
+
updated_at INTEGER NOT NULL
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 许可
|
|
192
|
+
|
|
193
|
+
与 Otto monorepo 相同。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { Server } from "node:http";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
interface ServerConfig {
|
|
6
|
+
/** SQLite 数据库文件目录,默认 process.cwd() */
|
|
7
|
+
dataDir: string;
|
|
8
|
+
/** 监听端口,默认 3001 */
|
|
9
|
+
port: number;
|
|
10
|
+
/** Bearer token(空字符串=不启用认证,仅开发环境) */
|
|
11
|
+
authToken: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* standalone 存储守护进程默认监听端口。族内单源——cli/remote-persistence-server.ts
|
|
15
|
+
* 复用此常量作为客户端默认,消除双源漂移。本包保持零 @otto 内部依赖(用裸 PORT env
|
|
16
|
+
* 而非 OTTO_*,见包头架构说明)。
|
|
17
|
+
*/
|
|
18
|
+
declare const REMOTE_PERSISTENCE_DEFAULT_PORT = 3001;
|
|
19
|
+
declare function loadConfig(env?: Record<string, string | undefined>): ServerConfig;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/db.d.ts
|
|
22
|
+
interface StoredSnapshot {
|
|
23
|
+
data: string;
|
|
24
|
+
revision: number;
|
|
25
|
+
}
|
|
26
|
+
interface SnapshotStore {
|
|
27
|
+
/**
|
|
28
|
+
* 条件写入:`expectedRevision===undefined` 表示恒无条件(兼容旧写入路径),
|
|
29
|
+
* 否则按服务端当前 revision 比对;成功返回新 revision,冲突返回 `null`。
|
|
30
|
+
*/
|
|
31
|
+
save(id: string, data: string, userId?: string, expectedRevision?: number): number | null;
|
|
32
|
+
load(id: string, userId?: string): StoredSnapshot | null;
|
|
33
|
+
currentRevision(id: string, userId?: string): number | null;
|
|
34
|
+
delete(id: string, userId?: string): boolean;
|
|
35
|
+
list(userId?: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* RFC-154 D1:轻量排序 id 列表——按 snapshot JSON 内 `metadata.lastActiveAt` 降序、排除
|
|
38
|
+
* `metadata.archived===true`,取前 limit 个。用 SQLite JSON1 的 `json_extract` 直接查询
|
|
39
|
+
* opaque 的 `data` 列,不需要 schema 迁移新增列(该 store 是通用 snapshot 存储,不止存
|
|
40
|
+
* session,不应为 session 专属字段加表结构)。非法/非 session snapshot(json_extract 返回
|
|
41
|
+
* null)按"未归档、lastActiveAt=0"处理,排在最后,不会被误排除。
|
|
42
|
+
*/
|
|
43
|
+
listRecentMeta(limit: number, userId?: string): string[];
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 追踪事件存储(append-only,session 分区)。
|
|
48
|
+
*
|
|
49
|
+
* - append:单条追加(session 内 seq 自增),返回分配 seq
|
|
50
|
+
* - read:按 session + seq 范围读取
|
|
51
|
+
* - truncate:截断到 toSeq(保留 seq < toSeq)
|
|
52
|
+
* - clear:删除整 session 的全部 trace
|
|
53
|
+
* - listSessions:列出有 trace 的 session id
|
|
54
|
+
*
|
|
55
|
+
* session_id 即 otto session id(UUID)。
|
|
56
|
+
*/
|
|
57
|
+
interface TraceLogStore {
|
|
58
|
+
append(userId: string, sessionId: string, data: string): number;
|
|
59
|
+
read(userId: string, sessionId: string, fromSeq?: number, toSeq?: number): string[];
|
|
60
|
+
/**
|
|
61
|
+
* session 事件总数(= 下一个待分配 seq)。COUNT(*) 索引扫描 O(log N)——
|
|
62
|
+
* 客户端(HttpAppendLog.count())据此在 backfill 时只拉最近 N 条,不必先做一次
|
|
63
|
+
* 全量 read 再数组长度(RFC-102 backlog T4)。
|
|
64
|
+
*/
|
|
65
|
+
count(userId: string, sessionId: string): number;
|
|
66
|
+
truncate(userId: string, sessionId: string, toSeq: number): void;
|
|
67
|
+
clear(userId: string, sessionId: string): void;
|
|
68
|
+
listSessions(userId: string): string[];
|
|
69
|
+
}
|
|
70
|
+
declare function createSnapshotStore(dbPath: string): SnapshotStore;
|
|
71
|
+
declare function createTraceLogStore(dbPath: string): TraceLogStore;
|
|
72
|
+
/**
|
|
73
|
+
* RFC-052 M71a:用户登录结果。
|
|
74
|
+
* `token` 为传入的原始 token(回显供客户端本地缓存)。
|
|
75
|
+
*/
|
|
76
|
+
interface UserLoginResult {
|
|
77
|
+
userId: string;
|
|
78
|
+
token: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* RFC-052 M71a:用户登录存储。
|
|
82
|
+
*
|
|
83
|
+
* 以 token 的 SHA-256 哈希为稳定 user_id,支持同一 token 多次登录得同一 user_id。
|
|
84
|
+
* MVP 不建独立 auth service——复用 remote-persistence-server 的 SQLite。
|
|
85
|
+
*/
|
|
86
|
+
declare function createUserLoginStore(dbPath: string): {
|
|
87
|
+
login: (token: string) => UserLoginResult;
|
|
88
|
+
close: () => void;
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/app.d.ts
|
|
92
|
+
declare function createApp(store: SnapshotStore, traceStore: TraceLogStore, userLogin: {
|
|
93
|
+
login: (token: string) => UserLoginResult;
|
|
94
|
+
}, config: ServerConfig): Hono;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/index.d.ts
|
|
97
|
+
interface StartServerResult {
|
|
98
|
+
app: Hono;
|
|
99
|
+
server: Server;
|
|
100
|
+
url: string;
|
|
101
|
+
store: SnapshotStore;
|
|
102
|
+
traceStore: TraceLogStore;
|
|
103
|
+
}
|
|
104
|
+
interface StartServerOptions {
|
|
105
|
+
host?: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 创建并启动 remote persistence server。
|
|
109
|
+
* 供 CLI 子命令和编程式嵌入使用。
|
|
110
|
+
*/
|
|
111
|
+
declare function startServer(config: ServerConfig, opts?: StartServerOptions): Promise<StartServerResult>;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { REMOTE_PERSISTENCE_DEFAULT_PORT, type ServerConfig, type SnapshotStore, StartServerOptions, StartServerResult, type TraceLogStore, type UserLoginResult, createApp, createSnapshotStore, createTraceLogStore, createUserLoginStore, loadConfig, startServer };
|
|
114
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/config.ts","../src/db.ts","../src/app.ts","../src/index.ts"],"mappings":";;;;UAAiB,YAAA;;EAEf,OAAA;;EAEA,IAAA;EAJ2B;EAM3B,SAAA;AAAA;;;;;;cAQW,+BAAA;AAAA,iBAEG,UAAA,CAAW,GAAA,GAAK,MAAA,+BAAmD,YAAA;;;UCLlE,cAAA;EACf,IAAA;EACA,QAAA;AAAA;AAAA,UAGe,aAAA;EDVf;;;AAQF;ECOE,IAAA,CAAK,EAAA,UAAY,IAAA,UAAc,MAAA,WAAiB,gBAAA;EAChD,IAAA,CAAK,EAAA,UAAY,MAAA,YAAkB,cAAA;EACnC,eAAA,CAAgB,EAAA,UAAY,MAAA;EAC5B,MAAA,CAAO,EAAA,UAAY,MAAA;EACnB,IAAA,CAAK,MAAA;EDTmB;;;;;;;ECiBxB,cAAA,CAAe,KAAA,UAAe,MAAA;EAC9B,KAAA;AAAA;;AAvBF;;;;;AAKA;;;;;UA4CiB,aAAA;EACf,MAAA,CAAO,MAAA,UAAgB,SAAA,UAAmB,IAAA;EAC1C,IAAA,CAAK,MAAA,UAAgB,SAAA,UAAmB,OAAA,WAAkB,KAAA;EAzCV;;;;;EA+ChD,KAAA,CAAM,MAAA,UAAgB,SAAA;EACtB,QAAA,CAAS,MAAA,UAAgB,SAAA,UAAmB,KAAA;EAC5C,KAAA,CAAM,MAAA,UAAgB,SAAA;EACtB,YAAA,CAAa,MAAA;AAAA;AAAA,iBAGC,mBAAA,CAAoB,MAAA,WAAiB,aAAA;AAAA,iBA4HrC,mBAAA,CAAoB,MAAA,WAAiB,aAAA;;;;;UA+EpC,eAAA;EACf,MAAA;EACA,KAAA;AAAA;;;;;;;iBASc,oBAAA,CAAqB,MAAA;EACnC,KAAA,GAAQ,KAAA,aAAkB,eAAA;EAC1B,KAAA;AAAA;;;iBCvRc,SAAA,CACd,KAAA,EAAO,aAAA,EACP,UAAA,EAAY,aAAA,EACZ,SAAA;EAAa,KAAA,GAAQ,KAAA,aAAkB,eAAA;AAAA,GACvC,MAAA,EAAQ,YAAA,GACP,IAAA;;;UCoBc,iBAAA;EACf,GAAA,EAAK,IAAA;EACL,MAAA,EAAQ,MAAA;EACR,GAAA;EACA,KAAA,EAAO,aAAA;EACP,UAAA,EAAY,aAAA;AAAA;AAAA,UAGG,kBAAA;EACf,IAAA;AAAA;AH7BF;;;;AAAA,iBGoCgB,WAAA,CACd,MAAA,EAAQ,YAAA,EACR,IAAA,GAAM,kBAAA,GACL,OAAA,CAAQ,iBAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import{mkdirSync as e}from"node:fs";import{join as t}from"node:path";import{serve as n}from"@hono/node-server";import r from"better-sqlite3";import{createHash as i,timingSafeEqual as a}from"node:crypto";import{Hono as o}from"hono";const s=3001;function c(e=process.env){let t=e.AUTH_TOKEN??``;return t||console.warn(`[remote-persistence-server] AUTH_TOKEN not set — server will start without authentication (development only)`),{dataDir:e.DATA_DIR??process.cwd(),port:parseInt(e.PORT??`3001`,10),authToken:t}}function l(e){return i(`sha256`).update(e).digest(`hex`)}function u(e){let t=new r(e);t.pragma(`journal_mode = WAL`),t.exec(`
|
|
2
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
data TEXT NOT NULL,
|
|
5
|
+
created_at INTEGER NOT NULL,
|
|
6
|
+
updated_at INTEGER NOT NULL
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_updated_at
|
|
10
|
+
ON snapshots(updated_at DESC);
|
|
11
|
+
`);let n=t.pragma(`table_info(snapshots)`).map(e=>e.name);n.includes(`user_id`)||t.exec(`ALTER TABLE snapshots ADD COLUMN user_id TEXT`),n.includes(`revision`)||t.exec(`ALTER TABLE snapshots ADD COLUMN revision INTEGER NOT NULL DEFAULT 1`),t.exec(`CREATE INDEX IF NOT EXISTS idx_snapshots_user_id ON snapshots(user_id)`);let i={insert:t.prepare(`INSERT OR IGNORE INTO snapshots (id, data, user_id, revision, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)`),load:t.prepare(`SELECT data, revision FROM snapshots WHERE id = ?`),load_user:t.prepare(`SELECT data, revision FROM snapshots WHERE id = ? AND (user_id = ? OR user_id IS NULL)`),delete:t.prepare(`DELETE FROM snapshots WHERE id = ?`),delete_user:t.prepare(`DELETE FROM snapshots WHERE id = ? AND (user_id = ? OR user_id IS NULL)`),list:t.prepare(`SELECT id FROM snapshots ORDER BY updated_at DESC`),list_user:t.prepare(`SELECT id FROM snapshots WHERE user_id = ? OR user_id IS NULL ORDER BY updated_at DESC`),listRecent:t.prepare(`SELECT id FROM snapshots
|
|
12
|
+
WHERE COALESCE(json_extract(data, '$.metadata.archived'), 0) = 0
|
|
13
|
+
ORDER BY COALESCE(json_extract(data, '$.metadata.lastActiveAt'), 0) DESC
|
|
14
|
+
LIMIT ?`),listRecent_user:t.prepare(`SELECT id FROM snapshots
|
|
15
|
+
WHERE (user_id = ? OR user_id IS NULL)
|
|
16
|
+
AND COALESCE(json_extract(data, '$.metadata.archived'), 0) = 0
|
|
17
|
+
ORDER BY COALESCE(json_extract(data, '$.metadata.lastActiveAt'), 0) DESC
|
|
18
|
+
LIMIT ?`)},a=(e,t)=>t?i.load_user.get(e,t):i.load.get(e),o=t.transaction((e,n,r,o)=>{let s=Date.now(),c=a(e,r);if(!c)return o!==void 0&&o!==0?{ok:!1}:(i.insert.run(e,n,r??null,s,s),{ok:!0,revision:1});if(o!==void 0&&c.revision!==o)return{ok:!1};let l=c.revision+1;return t.prepare(`UPDATE snapshots SET data = ?, user_id = COALESCE(?, user_id), revision = ?, updated_at = ? WHERE id = ?`).run(n,r??null,l,s,e),{ok:!0,revision:l}});return{save(e,t,n,r){let i=o(e,t,n,r);return i.ok?i.revision:null},load(e,t){let n=a(e,t);return n?{data:n.data,revision:n.revision}:null},currentRevision(e,t){return a(e,t)?.revision??null},delete(e,t){return(t?i.delete_user.run(e,t):i.delete.run(e)).changes>0},list(e){return(e?i.list_user.all(e):i.list.all()).map(e=>e.id)},listRecentMeta(e,t){return(t?i.listRecent_user.all(t,e):i.listRecent.all(e)).map(e=>e.id)},close(){t.close()}}}function d(e){let t=new r(e);t.pragma(`journal_mode = WAL`);let n=t.prepare(`PRAGMA table_info(trace_events)`).all();n.length>0&&!n.some(e=>e.name===`user_id`)&&t.exec(`ALTER TABLE trace_events ADD COLUMN user_id TEXT NOT NULL DEFAULT ""`),t.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS trace_events (
|
|
20
|
+
user_id TEXT NOT NULL DEFAULT '',
|
|
21
|
+
session_id TEXT NOT NULL,
|
|
22
|
+
seq INTEGER NOT NULL,
|
|
23
|
+
data TEXT NOT NULL,
|
|
24
|
+
ts INTEGER NOT NULL,
|
|
25
|
+
PRIMARY KEY (user_id, session_id, seq)
|
|
26
|
+
) WITHOUT ROWID;
|
|
27
|
+
`);let i={append:t.prepare(`
|
|
28
|
+
INSERT INTO trace_events (user_id, session_id, seq, data, ts)
|
|
29
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(seq), -1) + 1 FROM trace_events WHERE user_id = ? AND session_id = ?), ?, ?)
|
|
30
|
+
`),lastSeq:t.prepare(`SELECT COALESCE(MAX(seq), -1) as seq FROM trace_events WHERE user_id = ? AND session_id = ?`),read:t.prepare(`SELECT data FROM trace_events WHERE user_id = ? AND session_id = ? AND seq >= ? AND seq < ? ORDER BY seq ASC`),count:t.prepare(`SELECT COUNT(*) as n FROM trace_events WHERE user_id = ? AND session_id = ?`),truncate:t.prepare(`DELETE FROM trace_events WHERE user_id = ? AND session_id = ? AND seq >= ?`),clear:t.prepare(`DELETE FROM trace_events WHERE user_id = ? AND session_id = ?`),listSessions:t.prepare(`SELECT DISTINCT session_id FROM trace_events WHERE user_id = ? ORDER BY session_id`)};return{append(e,t,n){let r=Date.now();return i.append.run(e,t,e,t,n,r),i.lastSeq.get(e,t).seq},read(e,t,n,r){let a=n??0,o=r??2**53-1;return i.read.all(e,t,a,o).map(e=>e.data)},count(e,t){return i.count.get(e,t).n},truncate(e,t,n){i.truncate.run(e,t,n)},clear(e,t){i.clear.run(e,t)},listSessions(e){return i.listSessions.all(e).map(e=>e.session_id)}}}function f(e){let t=new r(e);t.pragma(`journal_mode = WAL`),t.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
32
|
+
user_id TEXT PRIMARY KEY,
|
|
33
|
+
token_hash TEXT NOT NULL,
|
|
34
|
+
created_at INTEGER NOT NULL
|
|
35
|
+
);
|
|
36
|
+
`);let n=l,i=t.prepare(`INSERT OR IGNORE INTO users (user_id, token_hash, created_at) VALUES (?, ?, ?)`),a=t.prepare(`SELECT user_id, token_hash FROM users WHERE token_hash = ?`);return{login(e){let t=n(e),r=Date.now();return i.run(t,t,r),{userId:a.get(t)?.user_id??t,token:e}},close(){t.close()}}}function p(e,t){return e.startsWith(`user:`)?t?e.split(`:`)[1]===t:!1:!0}function m(e,t){let n=new o;return n.get(`/health`,e=>e.json({ok:!0})),n.use(`/sessions`,t),n.use(`/sessions/*`,t),n.get(`/sessions/:wsKey`,t=>{try{let n=t.get(`userId`),r=e.list(n);return t.json({ids:r})}catch{return t.json({error:`storage_error`},500)}}),n.get(`/sessions/:wsKey/recent`,t=>{try{let n=t.get(`userId`),r=t.req.query(`limit`),i=Math.max(1,Math.min(1e3,Number(r)||50)),a=e.listRecentMeta(i,n);return t.json({ids:a})}catch{return t.json({error:`storage_error`},500)}}),n.get(`/sessions/:wsKey/:id`,t=>{try{let n=t.req.param(`id`),r=t.get(`userId`);if(!p(n,r))return t.json({error:`not_found`},404);let i=e.load(n,r);return i===null?t.json({error:`not_found`},404):(t.header(`ETag`,String(i.revision)),t.json(JSON.parse(i.data)))}catch{return t.json({error:`storage_error`},500)}}),n.put(`/sessions/:wsKey/:id`,async t=>{let n;try{n=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(n==null)return t.json({error:`invalid_body`},400);try{let r=t.req.param(`id`),i=t.get(`userId`);if(!p(r,i))return t.json({error:`forbidden`},403);let a=t.req.header(`If-Match`),o=a===void 0?void 0:Number(a.replace(/"/g,``));if(o!==void 0&&(!Number.isInteger(o)||o<0))return t.json({error:`invalid_if_match`},400);let s=e.save(r,JSON.stringify(n),i,o);if(s===null){let n=e.currentRevision(r,i);return t.json({error:`conflict`,revision:n},409)}return t.header(`ETag`,String(s)),t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),n.delete(`/sessions/:wsKey/:id`,t=>{try{let n=t.req.param(`id`),r=t.get(`userId`);return!p(n,r)||!e.delete(n,r)?t.json({error:`not_found`},404):t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),n}function h(e,t){let n=new o;return n.use(`/trace/*`,t),n.post(`/trace/:sessionId`,async t=>{let n=t.req.param(`sessionId`),r=t.get(`userId`)??``,i;try{i=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(i==null)return t.json({error:`invalid_body`},400);try{if(typeof i==`string`){let a=e.append(r,n,i);return t.json({seqs:[a]})}else if(Array.isArray(i)){let a=[];for(let t of i)typeof t==`string`&&a.push(e.append(r,n,t));return t.json({seqs:a})}else return t.json({error:`invalid_body`},400)}catch{return t.json({error:`storage_error`},500)}}),n.get(`/trace/:sessionId`,t=>{try{let n=t.req.param(`sessionId`),r=t.get(`userId`)??``,i=t.req.query(`from`),a=t.req.query(`to`),o=i===void 0?void 0:parseInt(i,10),s=a===void 0?void 0:parseInt(a,10);if(i!==void 0&&(isNaN(o)||o<0))return t.json({error:`invalid_from`},400);if(a!==void 0&&(isNaN(s)||s<0))return t.json({error:`invalid_to`},400);let c=e.read(r,n,o,s);return t.json({events:c})}catch{return t.json({error:`storage_error`},500)}}),n.get(`/trace/:sessionId/count`,t=>{try{let n=t.req.param(`sessionId`),r=t.get(`userId`)??``;return t.json({count:e.count(r,n)})}catch{return t.json({error:`storage_error`},500)}}),n.post(`/trace/:sessionId/truncate`,async t=>{let n=t.req.param(`sessionId`),r=t.get(`userId`)??``,i;try{i=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(typeof i?.toSeq!=`number`||i.toSeq<0)return t.json({error:`invalid_toSeq`},400);try{return e.truncate(r,n,i.toSeq),t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),n.delete(`/trace/:sessionId`,t=>{try{let n=t.req.param(`sessionId`),r=t.get(`userId`)??``;return e.clear(r,n),t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),n}function g(e,t){let n=new o;return n.post(`/auth/login`,async n=>{let r=n.req.header(`Authorization`);if(!r)return n.json({error:`missing authorization header`},401);let i=r.split(` `);if(i.length!==2||i[0]!==`Bearer`)return n.json({error:`invalid authorization scheme`},401);let o=i[1],s=new TextEncoder().encode(o),c=new TextEncoder().encode(e);if(s.length!==c.length||!a(s,c))return n.json({error:`invalid token`},401);let l=t.login(o);return n.json({user_id:l.userId,token:l.token})}),n}function _(e,t){let n=new o;n.use(`/config/*`,t);let r=e=>`user:${e}:config`;return n.get(`/config/user`,t=>{try{let n=t.get(`userId`);if(!n)return t.json({error:`missing_user_id`},400);let i=e.load(r(n),n);return i===null?t.json({error:`not_found`},404):t.json(JSON.parse(i.data))}catch{return t.json({error:`storage_error`},500)}}),n.put(`/config/user`,async t=>{let n;try{n=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(typeof n!=`object`||!n)return t.json({error:`invalid_body`},400);try{let i=t.get(`userId`);if(!i)return t.json({error:`missing_user_id`},400);let a=r(i),o=e.load(a,i),s=o?{...JSON.parse(o.data),...n}:n;return e.save(a,JSON.stringify(s),i),t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),n}function v(e,t,n,r){let i=new o,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,s=`${r}/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t)=>`user:${e}:${n}:${t}`;return i.get(s,t=>{try{let n=t.get(`userId`);if(!n)return t.json({error:`missing_user_id`},400);let r=t.req.param(`resourceId`);if(!r)return t.json({error:`missing_resource_id`},400);let i=e.load(c(n,r),n);return i===null?t.json({error:`not_found`},404):t.json(JSON.parse(i.data))}catch{return t.json({error:`storage_error`},500)}}),i.put(s,async t=>{let n;try{n=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(typeof n!=`object`||!n)return t.json({error:`invalid_body`},400);try{let r=t.get(`userId`);if(!r)return t.json({error:`missing_user_id`},400);let i=t.req.param(`resourceId`);return i?(e.save(c(r,i),JSON.stringify(n),r),t.json({ok:!0})):t.json({error:`missing_resource_id`},400)}catch{return t.json({error:`storage_error`},500)}}),i}function y(e,t,n,r){let i=new o,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,s=`${r}/:workspaceKey/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t,r)=>`workspace:${e}:${t}:${n}:${r}`;return i.get(s,t=>{try{let n=t.get(`userId`);if(!n)return t.json({error:`missing_user_id`},400);let r=t.req.param(`workspaceKey`),i=t.req.param(`resourceId`);if(!r||!i)return t.json({error:`missing_resource_id`},400);let a=e.load(c(n,r,i),n);return a===null?t.json({error:`not_found`},404):t.json(JSON.parse(a.data))}catch{return t.json({error:`storage_error`},500)}}),i.put(s,async t=>{let n;try{n=await t.req.json()}catch{return t.json({error:`invalid_body`},400)}if(typeof n!=`object`||!n)return t.json({error:`invalid_body`},400);try{let r=t.get(`userId`);if(!r)return t.json({error:`missing_user_id`},400);let i=t.req.param(`workspaceKey`),a=t.req.param(`resourceId`);if(!i||!a)return t.json({error:`missing_resource_id`},400);let o=c(r,i,a),s=n;if(typeof s.expectedVersion==`number`){let n=e.load(o,r);if(n!==null)try{let e=JSON.parse(n.data);if((typeof e.version==`number`?e.version:0)!==s.expectedVersion)return t.json({error:`version_conflict`},409)}catch{}}return e.save(o,JSON.stringify(n),r),t.json({ok:!0})}catch{return t.json({error:`storage_error`},500)}}),i}function b(e,t){return v(e,t,`memory`,`/memory/user`)}function x(e,t){return y(e,t,`memory`,`/memory/workspace`)}function S(e){let t=e!==``;return async(n,r)=>{if(!t)return r();let i=n.req.header(`Authorization`);if(!i)return n.json({error:`unauthorized`},401);let o=i.split(` `);if(o.length!==2||o[0]!==`Bearer`)return n.json({error:`forbidden`},403);let s=o[1],c=new TextEncoder().encode(s),u=new TextEncoder().encode(e);return c.length!==u.length||!a(c,u)?n.json({error:`forbidden`},403):(n.set(`userId`,l(s)),r())}}function C(e,t,n,r){let i=new o,a=S(r.authToken),s=m(e,a);i.route(`/`,s);let c=h(t,a);i.route(`/`,c);let l=g(r.authToken,n);i.route(`/`,l);let u=_(e,a);i.route(`/`,u);let d=b(e,a);i.route(`/`,d);let f=x(e,a);return i.route(`/`,f),i}function w(r,i={}){let a=i.host??`127.0.0.1`;e(r.dataDir,{recursive:!0});let o=t(r.dataDir,`otto.db`),s=u(o),c=d(o),l=C(s,c,f(o),r),p=n({fetch:l.fetch,port:r.port,hostname:a});return new Promise((e,t)=>{p.once(`listening`,()=>{let t=p.address(),n=typeof t==`object`&&t?t.port:r.port;console.log(`[remote-persistence-server] listening on http://${a===`0.0.0.0`?`localhost`:a}:${n}`),console.log(`[remote-persistence-server] auth: ${r.authToken?`enabled`:`DISABLED (dev only)`}`),e({app:l,server:p,url:`http://${a===`0.0.0.0`?`localhost`:a}:${n}`,store:s,traceStore:c})}),p.once(`error`,e=>{s.close(),t(e)})})}export{s as REMOTE_PERSISTENCE_DEFAULT_PORT,C as createApp,u as createSnapshotStore,d as createTraceLogStore,f as createUserLoginStore,c as loadConfig,w as startServer};
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/config.ts","../src/db.ts","../src/routes/storage.ts","../src/routes/trace.ts","../src/routes/auth.ts","../src/routes/config.ts","../src/routes/entity-route.ts","../src/routes/memory.ts","../src/middleware/auth.ts","../src/app.ts","../src/index.ts"],"sourcesContent":["export interface ServerConfig {\n /** SQLite 数据库文件目录,默认 process.cwd() */\n dataDir: string\n /** 监听端口,默认 3001 */\n port: number\n /** Bearer token(空字符串=不启用认证,仅开发环境) */\n authToken: string\n}\n\n/**\n * standalone 存储守护进程默认监听端口。族内单源——cli/remote-persistence-server.ts\n * 复用此常量作为客户端默认,消除双源漂移。本包保持零 @otto 内部依赖(用裸 PORT env\n * 而非 OTTO_*,见包头架构说明)。\n */\nexport const REMOTE_PERSISTENCE_DEFAULT_PORT = 3001\n\nexport function loadConfig(env: Record<string, string | undefined> = process.env): ServerConfig {\n const authToken = env['AUTH_TOKEN'] ?? ''\n\n if (!authToken) {\n console.warn(\n '[remote-persistence-server] AUTH_TOKEN not set — server will start without authentication (development only)',\n )\n }\n\n return {\n dataDir: env['DATA_DIR'] ?? process.cwd(),\n port: parseInt(env['PORT'] ?? String(REMOTE_PERSISTENCE_DEFAULT_PORT), 10),\n authToken,\n }\n}\n","import Database from 'better-sqlite3'\nimport { createHash } from 'node:crypto'\n\n/**\n * 从 bearer token 派生稳定 user_id(SHA-256 hex)。单一真源——auth 中间件(分区身份)与\n * 用户登录存储(users.user_id)共用,保证「同一 token → 同一 user_id」(RFC-074 R-SEC)。\n */\nexport function deriveUserId(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\nexport interface StoredSnapshot {\n data: string\n revision: number\n}\n\nexport interface SnapshotStore {\n /**\n * 条件写入:`expectedRevision===undefined` 表示恒无条件(兼容旧写入路径),\n * 否则按服务端当前 revision 比对;成功返回新 revision,冲突返回 `null`。\n */\n save(id: string, data: string, userId?: string, expectedRevision?: number): number | null\n load(id: string, userId?: string): StoredSnapshot | null\n currentRevision(id: string, userId?: string): number | null\n delete(id: string, userId?: string): boolean\n list(userId?: string): string[]\n /**\n * RFC-154 D1:轻量排序 id 列表——按 snapshot JSON 内 `metadata.lastActiveAt` 降序、排除\n * `metadata.archived===true`,取前 limit 个。用 SQLite JSON1 的 `json_extract` 直接查询\n * opaque 的 `data` 列,不需要 schema 迁移新增列(该 store 是通用 snapshot 存储,不止存\n * session,不应为 session 专属字段加表结构)。非法/非 session snapshot(json_extract 返回\n * null)按\"未归档、lastActiveAt=0\"处理,排在最后,不会被误排除。\n */\n listRecentMeta(limit: number, userId?: string): string[]\n close(): void\n}\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_snapshots_updated_at\n ON snapshots(updated_at DESC);\n`\n\n/**\n * 追踪事件存储(append-only,session 分区)。\n *\n * - append:单条追加(session 内 seq 自增),返回分配 seq\n * - read:按 session + seq 范围读取\n * - truncate:截断到 toSeq(保留 seq < toSeq)\n * - clear:删除整 session 的全部 trace\n * - listSessions:列出有 trace 的 session id\n *\n * session_id 即 otto session id(UUID)。\n */\nexport interface TraceLogStore {\n append(userId: string, sessionId: string, data: string): number\n read(userId: string, sessionId: string, fromSeq?: number, toSeq?: number): string[]\n /**\n * session 事件总数(= 下一个待分配 seq)。COUNT(*) 索引扫描 O(log N)——\n * 客户端(HttpAppendLog.count())据此在 backfill 时只拉最近 N 条,不必先做一次\n * 全量 read 再数组长度(RFC-102 backlog T4)。\n */\n count(userId: string, sessionId: string): number\n truncate(userId: string, sessionId: string, toSeq: number): void\n clear(userId: string, sessionId: string): void\n listSessions(userId: string): string[]\n}\n\nexport function createSnapshotStore(dbPath: string): SnapshotStore {\n const db = new Database(dbPath)\n db.pragma('journal_mode = WAL')\n db.exec(SCHEMA)\n\n const columns = (db.pragma('table_info(snapshots)') as Array<{ name: string }>).map(\n (c) => c.name,\n )\n if (!columns.includes('user_id')) {\n db.exec('ALTER TABLE snapshots ADD COLUMN user_id TEXT')\n }\n if (!columns.includes('revision')) {\n // 既有行回填为 1(而非 0),保证首次 CAS 请求 expectedRevision=1 与基线一致。\n db.exec('ALTER TABLE snapshots ADD COLUMN revision INTEGER NOT NULL DEFAULT 1')\n }\n db.exec('CREATE INDEX IF NOT EXISTS idx_snapshots_user_id ON snapshots(user_id)')\n\n const stmts = {\n insert: db.prepare(\n 'INSERT OR IGNORE INTO snapshots (id, data, user_id, revision, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)',\n ),\n load: db.prepare('SELECT data, revision FROM snapshots WHERE id = ?'),\n load_user: db.prepare(\n 'SELECT data, revision FROM snapshots WHERE id = ? AND (user_id = ? OR user_id IS NULL)',\n ),\n delete: db.prepare('DELETE FROM snapshots WHERE id = ?'),\n delete_user: db.prepare(\n 'DELETE FROM snapshots WHERE id = ? AND (user_id = ? OR user_id IS NULL)',\n ),\n list: db.prepare('SELECT id FROM snapshots ORDER BY updated_at DESC'),\n list_user: db.prepare(\n 'SELECT id FROM snapshots WHERE user_id = ? OR user_id IS NULL ORDER BY updated_at DESC',\n ),\n // RFC-154 D1:COALESCE(json_extract(...), 0) 让非 session/无 metadata 的 snapshot\n // 落到\"未归档、lastActiveAt=0\"(排最后,不误排除)。json_extract 对非法 JSON/缺字段\n // 返回 NULL,不抛错——data 列本就是 opaque 存储,不能假设都是 session snapshot。\n listRecent: db.prepare(\n `SELECT id FROM snapshots\n WHERE COALESCE(json_extract(data, '$.metadata.archived'), 0) = 0\n ORDER BY COALESCE(json_extract(data, '$.metadata.lastActiveAt'), 0) DESC\n LIMIT ?`,\n ),\n listRecent_user: db.prepare(\n `SELECT id FROM snapshots\n WHERE (user_id = ? OR user_id IS NULL)\n AND COALESCE(json_extract(data, '$.metadata.archived'), 0) = 0\n ORDER BY COALESCE(json_extract(data, '$.metadata.lastActiveAt'), 0) DESC\n LIMIT ?`,\n ),\n }\n\n const loadRow = (id: string, userId?: string): { data: string; revision: number } | undefined =>\n userId\n ? (stmts.load_user.get(id, userId) as { data: string; revision: number } | undefined)\n : (stmts.load.get(id) as { data: string; revision: number } | undefined)\n\n const saveTxn = db.transaction(\n (id: string, data: string, userId: string | undefined, expectedRevision: number | undefined) => {\n const now = Date.now()\n const existing = loadRow(id, userId)\n\n if (!existing) {\n // 新文档:仅当客户端明确要求“新建且期望无 prior”时才允许 expectedRevision===0。\n // 出于兼容与宽容性,缺省/未提供 expectedRevision 的旧客户端仍允许直接新建,\n // 但此时 revision 从 1 开始且不比对。\n if (expectedRevision !== undefined && expectedRevision !== 0) {\n return { ok: false as const }\n }\n stmts.insert.run(id, data, userId ?? null, now, now)\n return { ok: true as const, revision: 1 }\n }\n\n if (expectedRevision !== undefined && existing.revision !== expectedRevision) {\n return { ok: false as const }\n }\n\n const nextRevision = existing.revision + 1\n db.prepare(\n 'UPDATE snapshots SET data = ?, user_id = COALESCE(?, user_id), revision = ?, updated_at = ? WHERE id = ?',\n ).run(data, userId ?? null, nextRevision, now, id)\n return { ok: true as const, revision: nextRevision }\n },\n )\n\n return {\n save(id: string, data: string, userId?: string, expectedRevision?: number): number | null {\n const result = saveTxn(id, data, userId, expectedRevision)\n return result.ok ? result.revision : null\n },\n\n load(id: string, userId?: string): StoredSnapshot | null {\n const row = loadRow(id, userId)\n return row ? { data: row.data, revision: row.revision } : null\n },\n\n currentRevision(id: string, userId?: string): number | null {\n return loadRow(id, userId)?.revision ?? null\n },\n\n delete(id: string, userId?: string): boolean {\n const result = userId ? stmts.delete_user.run(id, userId) : stmts.delete.run(id)\n return result.changes > 0\n },\n\n list(userId?: string): string[] {\n const rows = userId\n ? (stmts.list_user.all(userId) as { id: string }[])\n : (stmts.list.all() as { id: string }[])\n return rows.map((r) => r.id)\n },\n\n listRecentMeta(limit: number, userId?: string): string[] {\n const rows = userId\n ? (stmts.listRecent_user.all(userId, limit) as { id: string }[])\n : (stmts.listRecent.all(limit) as { id: string }[])\n return rows.map((r) => r.id)\n },\n\n close(): void {\n db.close()\n },\n }\n}\n\nexport function createTraceLogStore(dbPath: string): TraceLogStore {\n const db = new Database(dbPath)\n db.pragma('journal_mode = WAL')\n\n // RFC-353: trace_events 加 user_id 列实现租户隔离(认证 ≠ 授权)。\n // 新表直接建带 user_id;旧表(无 user_id 列)迁移加列后重建索引。\n // 主键改为 (user_id, session_id, seq)——不同用户的同名 session 互不可见。\n const hasUserIdCol = db.prepare(\"PRAGMA table_info(trace_events)\").all() as { name: string }[]\n if (hasUserIdCol.length > 0 && !hasUserIdCol.some((c) => c.name === 'user_id')) {\n db.exec('ALTER TABLE trace_events ADD COLUMN user_id TEXT NOT NULL DEFAULT \"\"')\n }\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS trace_events (\n user_id TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n ts INTEGER NOT NULL,\n PRIMARY KEY (user_id, session_id, seq)\n ) WITHOUT ROWID;\n `)\n\n const stmts = {\n append: db.prepare(`\n INSERT INTO trace_events (user_id, session_id, seq, data, ts)\n VALUES (?, ?, (SELECT COALESCE(MAX(seq), -1) + 1 FROM trace_events WHERE user_id = ? AND session_id = ?), ?, ?)\n `),\n lastSeq: db.prepare(\n 'SELECT COALESCE(MAX(seq), -1) as seq FROM trace_events WHERE user_id = ? AND session_id = ?',\n ),\n read: db.prepare(\n 'SELECT data FROM trace_events WHERE user_id = ? AND session_id = ? AND seq >= ? AND seq < ? ORDER BY seq ASC',\n ),\n count: db.prepare('SELECT COUNT(*) as n FROM trace_events WHERE user_id = ? AND session_id = ?'),\n truncate: db.prepare('DELETE FROM trace_events WHERE user_id = ? AND session_id = ? AND seq >= ?'),\n clear: db.prepare('DELETE FROM trace_events WHERE user_id = ? AND session_id = ?'),\n listSessions: db.prepare('SELECT DISTINCT session_id FROM trace_events WHERE user_id = ? ORDER BY session_id'),\n }\n\n return {\n append(userId: string, sessionId: string, data: string): number {\n const now = Date.now()\n stmts.append.run(userId, sessionId, userId, sessionId, data, now)\n const row = stmts.lastSeq.get(userId, sessionId) as { seq: number }\n return row.seq\n },\n\n read(userId: string, sessionId: string, fromSeq?: number, toSeq?: number): string[] {\n const from = fromSeq ?? 0\n const to = toSeq ?? Number.MAX_SAFE_INTEGER\n const rows = stmts.read.all(userId, sessionId, from, to) as { data: string }[]\n return rows.map((r) => r.data)\n },\n\n count(userId: string, sessionId: string): number {\n const row = stmts.count.get(userId, sessionId) as { n: number }\n return row.n\n },\n\n truncate(userId: string, sessionId: string, toSeq: number): void {\n stmts.truncate.run(userId, sessionId, toSeq)\n },\n\n clear(userId: string, sessionId: string): void {\n stmts.clear.run(userId, sessionId)\n },\n\n listSessions(userId: string): string[] {\n const rows = stmts.listSessions.all(userId) as { session_id: string }[]\n return rows.map((r) => r.session_id)\n },\n }\n}\n\n/**\n * RFC-052 M71a:用户登录结果。\n * `token` 为传入的原始 token(回显供客户端本地缓存)。\n */\nexport interface UserLoginResult {\n userId: string\n token: string\n}\n\n/**\n * RFC-052 M71a:用户登录存储。\n *\n * 以 token 的 SHA-256 哈希为稳定 user_id,支持同一 token 多次登录得同一 user_id。\n * MVP 不建独立 auth service——复用 remote-persistence-server 的 SQLite。\n */\nexport function createUserLoginStore(dbPath: string): {\n login: (token: string) => UserLoginResult\n close: () => void\n} {\n const db = new Database(dbPath)\n db.pragma('journal_mode = WAL')\n db.exec(`\n CREATE TABLE IF NOT EXISTS users (\n user_id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL,\n created_at INTEGER NOT NULL\n );\n `)\n\n const hashToken = deriveUserId\n\n const loginStmt = db.prepare(\n 'INSERT OR IGNORE INTO users (user_id, token_hash, created_at) VALUES (?, ?, ?)',\n )\n const findStmt = db.prepare('SELECT user_id, token_hash FROM users WHERE token_hash = ?')\n\n return {\n login(token: string): UserLoginResult {\n const hash = hashToken(token)\n const now = Date.now()\n\n loginStmt.run(hash, hash, now)\n\n const row = findStmt.get(hash) as { user_id: string; token_hash: string } | undefined\n const userId = row?.user_id ?? hash\n\n return { userId, token }\n },\n\n close(): void {\n db.close()\n },\n }\n}\n","import { Hono } from 'hono'\nimport type { SnapshotStore } from '../db'\nimport type { MiddlewareHandler } from 'hono'\nimport type { AuthEnv } from '../middleware/auth'\n\n/**\n * M71c: `user:<userId>:*` key 前缀校验——阻止跨 user 读写/覆写。\n * 非 user key(如 ws key → session snapshots)不做前缀校验(session id 天然唯一)。\n */\nfunction requireUserKeyMatch(id: string, userId: string | undefined): boolean {\n if (!id.startsWith('user:')) return true\n if (!userId) return false\n return id.split(':')[1] === userId\n}\n\n/**\n * 创建 storage 路由(6 REST 端点 + health)。\n *\n * 契约精确对齐 RemotePersistence 协议(RFC-077 M1 修正 sp-4:客户端基类 B12 起恒带\n * `/{wsKey}` 路径前缀,本服务路由必须含 `:wsKey` 段,否则客户端 `/sessions/default/<id>`\n * 撞不上扁平 `/sessions/:id` → 静默 404、save 丢数据 / load 返 null):\n * - GET /sessions/:wsKey → { ids: string[] }\n * - GET /sessions/:wsKey/recent → { ids: string[] }(RFC-154 D1,见下)\n * - GET /sessions/:wsKey/:id → Snapshot JSON | 404\n * - PUT /sessions/:wsKey/:id → { ok: true }\n * - DELETE /sessions/:wsKey/:id → { ok: true } | 404\n *\n * `:wsKey` 在本「极简 headless 单租户后端」accept-and-ignore——分区由 userId(token 派生,\n * M71b/c)+ 全局唯一 session id 承担(见 requireUserKeyMatch 注:session id 天然唯一);\n * 跨 workspace 物理隔离属 sp-3 内核收敛(backlog),不在本修范围。\n *\n * M71c: user key 前缀校验(key=`user:<userId>:*` 时必须 userId header 匹配 key 前缀)。\n *\n * RFC-154 D1:`/recent` 是独立新增的专用轻量端点(不是给既有 `listPaginated` 补 sortBy 参数)——\n * 现有 `?page/pageSize/sortBy/order` query 从未被本路由处理(`GET /sessions/:wsKey` 完全忽略\n * query,恒返回全量 `{ids}`),客户端 `RemotePersistence.listPaginated()` 期望的\n * `PaginatedResult<string>` 形状(`items/total/page/...`)与本路由实际返回的 `{ids}` 完全不匹配,\n * 是一个既存的、比\"缺 sortBy\"更严重的契约缺口。修复该缺口工作量超出本 RFC 范围,记录为独立\n * backlog;`/recent` 端点职责单一(仅排序 id 列表,无分页语义),不依赖、也不修复该缺口。\n * 路由注册顺序:`/recent` 必须在 `/:id` 之前,否则 Hono 会把 `recent` 当作 `:id` 参数匹配掉。\n */\nexport function createStorageRoute(store: SnapshotStore, auth: MiddlewareHandler<AuthEnv>): Hono<AuthEnv> {\n const app = new Hono<AuthEnv>()\n\n app.get('/health', (c) => c.json({ ok: true }))\n\n // R-SEC:裸 `/sessions` 与 `/sessions/*` 都必须过 auth(Hono `/sessions/*` 通配不匹配自身)。\n app.use('/sessions', auth)\n app.use('/sessions/*', auth)\n\n app.get('/sessions/:wsKey', (c) => {\n try {\n const userId = c.get('userId')\n const ids = store.list(userId)\n return c.json({ ids })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n // RFC-154 D1:必须注册在 `/sessions/:wsKey/:id` 之前(路由匹配顺序,见头注)。\n app.get('/sessions/:wsKey/recent', (c) => {\n try {\n const userId = c.get('userId')\n const limitParam = c.req.query('limit')\n const limit = Math.max(1, Math.min(1000, Number(limitParam) || 50))\n const ids = store.listRecentMeta(limit, userId)\n return c.json({ ids })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.get('/sessions/:wsKey/:id', (c) => {\n try {\n const id = c.req.param('id')\n const userId = c.get('userId')\n if (!requireUserKeyMatch(id, userId)) {\n return c.json({ error: 'not_found' }, 404)\n }\n const snap = store.load(id, userId)\n if (snap === null) {\n return c.json({ error: 'not_found' }, 404)\n }\n // revision 经 ETag 暴露,客户端据此发起下次 If-Match CAS。\n c.header('ETag', String(snap.revision))\n return c.json(JSON.parse(snap.data) as unknown)\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.put('/sessions/:wsKey/:id', async (c) => {\n let body: unknown\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (body === undefined || body === null) {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n try {\n const id = c.req.param('id')\n const userId = c.get('userId')\n if (!requireUserKeyMatch(id, userId)) {\n return c.json({ error: 'forbidden' }, 403)\n }\n\n // If-Match 是 CAS 的显式 revision;未带时按 undefined 走无条件兼容写。\n const ifMatch = c.req.header('If-Match')\n const expectedRevision = ifMatch === undefined ? undefined : Number(ifMatch.replace(/\"/g, ''))\n if (expectedRevision !== undefined && (!Number.isInteger(expectedRevision) || expectedRevision < 0)) {\n return c.json({ error: 'invalid_if_match' }, 400)\n }\n\n const newRevision = store.save(id, JSON.stringify(body), userId, expectedRevision)\n if (newRevision === null) {\n const current = store.currentRevision(id, userId)\n return c.json({ error: 'conflict', revision: current }, 409)\n }\n c.header('ETag', String(newRevision))\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.delete('/sessions/:wsKey/:id', (c) => {\n try {\n const id = c.req.param('id')\n const userId = c.get('userId')\n if (!requireUserKeyMatch(id, userId)) {\n return c.json({ error: 'not_found' }, 404)\n }\n const removed = store.delete(id, userId)\n if (!removed) {\n return c.json({ error: 'not_found' }, 404)\n }\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n return app\n}\n","import { Hono } from 'hono'\nimport type { TraceLogStore } from '../db'\nimport type { AuthEnv } from '../middleware/auth'\nimport type { MiddlewareHandler } from 'hono'\n\n/**\n * Trace 路由(append-only 事件流,session 分区)。\n *\n * RFC-353: 租户授权——userId 从 auth 中间件上下文获取(服务端从 token 派生,不信客户端头),\n * 透传到 TraceLogStore 做物理隔离。不同 userId 的同名 session 互不可见。\n *\n * 契约对齐 AppendLog<TraceEvent> 语义:\n * - POST /trace/:sessionId → 追加事件(单条或数组),返回 { seqs: number[] }\n * - GET /trace/:sessionId → 按 seq 范围读取,返回 { events: string[] }\n * - GET /trace/:sessionId/count → session 事件总数,返回 { count: number }\n * - POST /trace/:sessionId/truncate → 截断到 toSeq(body: { toSeq: number })\n * - DELETE /trace/:sessionId → 清空整 session trace\n */\nexport function createTraceRoute(store: TraceLogStore, auth: MiddlewareHandler<AuthEnv>): Hono<AuthEnv> {\n const app = new Hono<AuthEnv>()\n\n app.use('/trace/*', auth)\n\n app.post('/trace/:sessionId', async (c) => {\n const sessionId = c.req.param('sessionId')\n const userId = c.get('userId') ?? ''\n\n let body: unknown\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (body === undefined || body === null) {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n try {\n if (typeof body === 'string') {\n const seq = store.append(userId, sessionId, body)\n return c.json({ seqs: [seq] })\n } else if (Array.isArray(body)) {\n const seqs: number[] = []\n for (const item of body) {\n if (typeof item === 'string') {\n seqs.push(store.append(userId, sessionId, item))\n }\n }\n return c.json({ seqs })\n } else {\n return c.json({ error: 'invalid_body' }, 400)\n }\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.get('/trace/:sessionId', (c) => {\n try {\n const sessionId = c.req.param('sessionId')\n const userId = c.get('userId') ?? ''\n const fromStr = c.req.query('from')\n const toStr = c.req.query('to')\n\n const from = fromStr !== undefined ? parseInt(fromStr, 10) : undefined\n const to = toStr !== undefined ? parseInt(toStr, 10) : undefined\n\n if (fromStr !== undefined && (isNaN(from as number) || from! < 0)) {\n return c.json({ error: 'invalid_from' }, 400)\n }\n if (toStr !== undefined && (isNaN(to as number) || to! < 0)) {\n return c.json({ error: 'invalid_to' }, 400)\n }\n\n const lines = store.read(userId, sessionId, from, to)\n return c.json({ events: lines })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n // /trace/:sessionId 只匹配单段路径,/trace/xxx/count 是两段——与上面的 GET 路由\n // 天然不冲突(非路由优先级问题)。放在此处仅为文档阅读顺序。\n app.get('/trace/:sessionId/count', (c) => {\n try {\n const sessionId = c.req.param('sessionId')\n const userId = c.get('userId') ?? ''\n return c.json({ count: store.count(userId, sessionId) })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.post('/trace/:sessionId/truncate', async (c) => {\n const sessionId = c.req.param('sessionId')\n const userId = c.get('userId') ?? ''\n\n let body: { toSeq?: number }\n try {\n body = (await c.req.json()) as { toSeq?: number }\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (typeof body?.toSeq !== 'number' || body.toSeq < 0) {\n return c.json({ error: 'invalid_toSeq' }, 400)\n }\n\n try {\n store.truncate(userId, sessionId, body.toSeq)\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.delete('/trace/:sessionId', (c) => {\n try {\n const sessionId = c.req.param('sessionId')\n const userId = c.get('userId') ?? ''\n store.clear(userId, sessionId)\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n return app\n}\n","import { Hono } from 'hono'\nimport { timingSafeEqual } from 'node:crypto'\nimport type { UserLoginResult } from '../db'\n\n/**\n * RFC-052 M71a:/auth/login 路由。\n *\n * POST /auth/login(Header: Authorization: Bearer <token>)\n * → 验证 token 与 server authToken 一致 → 创建/查找 user → 返回 {user_id, token}\n *\n * MVP:token 即 server authToken(复用 remote-persistence-server 既有鉴权)。\n * 多用户共享同一 service token 时得不同 user_id(token_hash=SHA-256 作为稳定 id)。\n */\nexport function createAuthRoute(\n authToken: string,\n userLogin: { login: (token: string) => UserLoginResult },\n): Hono {\n const app = new Hono()\n\n app.post('/auth/login', async (c) => {\n const header = c.req.header('Authorization')\n\n if (!header) {\n return c.json({ error: 'missing authorization header' }, 401)\n }\n\n const parts = header.split(' ')\n if (parts.length !== 2 || parts[0] !== 'Bearer') {\n return c.json({ error: 'invalid authorization scheme' }, 401)\n }\n\n const supplied = parts[1]!\n\n const sa = new TextEncoder().encode(supplied)\n const sb = new TextEncoder().encode(authToken)\n\n if (sa.length !== sb.length || !timingSafeEqual(sa, sb)) {\n return c.json({ error: 'invalid token' }, 401)\n }\n\n const result = userLogin.login(supplied)\n\n return c.json({\n user_id: result.userId,\n token: result.token,\n })\n })\n\n return app\n}\n","import { Hono } from 'hono'\nimport type { SnapshotStore } from '../db'\nimport type { MiddlewareHandler } from 'hono'\nimport type { AuthEnv } from '../middleware/auth'\n\n/**\n * RFC-052 M71d:用户配置同步端点。\n *\n * 存储 key = `user:<userId>:config`(复用 snapshots 表,按 user_id 分区)。\n *\n * - GET /config/user → 返回 syncable 字段子集(JSON)| 404\n * - PUT /config/user → 合并写入(读-改-写)| 200\n *\n * 鉴权:与 storage 端点共用同一 auth middleware;userId 由 token 服务端派生(R-SEC,不信客户端头)。\n */\nexport function createConfigRoute(store: SnapshotStore, auth: MiddlewareHandler<AuthEnv>): Hono<AuthEnv> {\n const app = new Hono<AuthEnv>()\n\n app.use('/config/*', auth)\n\n const CONFIG_KEY = (userId: string): string => `user:${userId}:config`\n\n app.get('/config/user', (c) => {\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n const snap = store.load(CONFIG_KEY(userId), userId)\n if (snap === null) {\n return c.json({ error: 'not_found' }, 404)\n }\n return c.json(JSON.parse(snap.data) as unknown)\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.put('/config/user', async (c) => {\n let body: unknown\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (body === undefined || body === null || typeof body !== 'object') {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n\n const key = CONFIG_KEY(userId)\n const existing = store.load(key, userId)\n const merged = existing\n ? {\n ...(JSON.parse(existing.data) as Record<string, unknown>),\n ...(body as Record<string, unknown>),\n }\n : (body as Record<string, unknown>)\n store.save(key, JSON.stringify(merged), userId)\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n return app\n}\n","import { Hono } from 'hono'\nimport type { SnapshotStore } from '../db'\nimport type { MiddlewareHandler } from 'hono'\nimport type { AuthEnv } from '../middleware/auth'\n\n/**\n * RFC-108 D1:通用实体路由工厂。\n *\n * 每种实体类型(如 memory)使用 key = `user:<userId>:<entityKind>:<resourceId>`,\n * 提供一对 GET/PUT 端点,路径为 `${pathSegment}/:resourceId`(如 `/memory/user/:resourceId`)。\n *\n * **多资源修正**:本实体路由必须支持同一 entityKind 下的多个独立资源(如 memory 场景下\n * `AGENTS.md`/`MEMORY.md`/`memory/<slug>.md` 是互相独立、各自可覆盖写的文件——固定单一\n * key 会导致写 A 文件覆盖 B 文件的数据丢失)。`resourceId` 由调用方(如 HttpMemoryStore)\n * 传入文件相对路径的 URL-safe 编码。\n *\n * 语义差异(与 config.ts 对比):\n * - config.ts 的 PUT 做**读-改-写合并**(单一 config 资源,字段可独立更新,合并语义合理)。\n * - 本路由的 PUT 做**直接覆盖写**(每个 resourceId 对应完整文件内容字符串/大 blob,\n * 不是可合并的字段集合;读-改-写合并无意义且会引入并发写丢数据风险)。\n *\n * 鉴权:通过 auth middleware,userId 由 token 服务端派生(R-SEC,不信客户端头)。\n */\nexport function createEntityRoute(\n store: SnapshotStore,\n auth: MiddlewareHandler<AuthEnv>,\n entityKind: string,\n pathSegment: string,\n): Hono<AuthEnv> {\n const app = new Hono<AuthEnv>()\n\n const prefix = pathSegment.split('/').slice(0, -1).join('/') || '/'\n const resourcePath = `${pathSegment}/:resourceId`\n\n // 在父路径上挂载 auth middleware(如 /memory/* 覆盖 /memory/user/*)\n app.use(`${prefix}/*`, auth)\n app.use(`${pathSegment}/*`, auth)\n\n const ENTITY_KEY = (userId: string, resourceId: string): string =>\n `user:${userId}:${entityKind}:${resourceId}`\n\n app.get(resourcePath, (c) => {\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n const resourceId = c.req.param('resourceId')\n if (!resourceId) {\n return c.json({ error: 'missing_resource_id' }, 400)\n }\n const snap = store.load(ENTITY_KEY(userId, resourceId), userId)\n if (snap === null) {\n return c.json({ error: 'not_found' }, 404)\n }\n return c.json(JSON.parse(snap.data) as unknown)\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.put(resourcePath, async (c) => {\n let body: unknown\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (body === undefined || body === null || typeof body !== 'object') {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n const resourceId = c.req.param('resourceId')\n if (!resourceId) {\n return c.json({ error: 'missing_resource_id' }, 400)\n }\n\n // 直接覆盖写(不合并)—— 每个 resourceId 对应完整文件内容字符串/大 blob,\n // 不是可合并的字段集合(与 config.ts 的读-改-写合并语义不同)。\n store.save(ENTITY_KEY(userId, resourceId), JSON.stringify(body), userId)\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n return app\n}\n\n/**\n * RFC-164 M164-2 D5(安全修正):workspace 级实体路由——与 `createEntityRoute` 不复用,\n * 因其 `ENTITY_KEY` 硬编码 `user:<userId>:<kind>:<resourceId>`(无 workspace 维度)。\n *\n * key = `workspace:<userId>:<workspaceKey>:<resourceId>`——`userId`(token 服务端派生)\n * 继续担任唯一安全边界(不信任客户端可控的 `workspaceKey` 路径参数作隔离依据,与\n * `createEntityRoute`/`storage.ts` 的 `/sessions/:wsKey` 既有安全模式一致——那里 `wsKey`\n * 同样只是路径占位,真正隔离靠 `userId`);`workspaceKey` 是同一用户名下的进一步分区\n * (区分该用户名下的不同项目)。\n *\n * D6 版本冲突检测(PUT body 携带 `version`/`expectedVersion`):不扩展 `SnapshotStore`\n * schema——`data` 列本身是不透明字符串,直接在路由内解析既有内容里的 `version` 字段\n * 与请求 `expectedVersion` 比对,不一致返回 409(由 `HttpMemoryStore` 转存冲突旁路文件)。\n *\n * 路由:`${pathSegment}/:workspaceKey/:resourceId`(如 `/memory/workspace/:workspaceKey/:resourceId`)。\n */\nexport function createWorkspaceEntityRoute(\n store: SnapshotStore,\n auth: MiddlewareHandler<AuthEnv>,\n entityKind: string,\n pathSegment: string,\n): Hono<AuthEnv> {\n const app = new Hono<AuthEnv>()\n\n const prefix = pathSegment.split('/').slice(0, -1).join('/') || '/'\n const resourcePath = `${pathSegment}/:workspaceKey/:resourceId`\n\n app.use(`${prefix}/*`, auth)\n app.use(`${pathSegment}/*`, auth)\n\n const ENTITY_KEY = (userId: string, workspaceKey: string, resourceId: string): string =>\n `workspace:${userId}:${workspaceKey}:${entityKind}:${resourceId}`\n\n app.get(resourcePath, (c) => {\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n const workspaceKey = c.req.param('workspaceKey')\n const resourceId = c.req.param('resourceId')\n if (!workspaceKey || !resourceId) {\n return c.json({ error: 'missing_resource_id' }, 400)\n }\n const snap = store.load(ENTITY_KEY(userId, workspaceKey, resourceId), userId)\n if (snap === null) {\n return c.json({ error: 'not_found' }, 404)\n }\n return c.json(JSON.parse(snap.data) as unknown)\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n app.put(resourcePath, async (c) => {\n let body: unknown\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n if (body === undefined || body === null || typeof body !== 'object') {\n return c.json({ error: 'invalid_body' }, 400)\n }\n\n try {\n const userId = c.get('userId')\n if (!userId) {\n return c.json({ error: 'missing_user_id' }, 400)\n }\n const workspaceKey = c.req.param('workspaceKey')\n const resourceId = c.req.param('resourceId')\n if (!workspaceKey || !resourceId) {\n return c.json({ error: 'missing_resource_id' }, 400)\n }\n\n const key = ENTITY_KEY(userId, workspaceKey, resourceId)\n const bodyObj = body as { expectedVersion?: unknown }\n\n // D6 冲突检测:expectedVersion 显式提供时,与既有存储内容的 version 字段比对。\n if (typeof bodyObj.expectedVersion === 'number') {\n const existingSnap = store.load(key, userId)\n if (existingSnap !== null) {\n try {\n const existing = JSON.parse(existingSnap.data) as { version?: unknown }\n const existingVersion = typeof existing.version === 'number' ? existing.version : 0\n if (existingVersion !== bodyObj.expectedVersion) {\n return c.json({ error: 'version_conflict' }, 409)\n }\n } catch {\n // 既有内容非法 JSON——视为无版本历史,放行写入(fail-open 到\"直接覆盖\",\n // 与本路由整体的直接覆盖写语义一致)。\n }\n }\n }\n\n store.save(key, JSON.stringify(body), userId)\n return c.json({ ok: true })\n } catch {\n return c.json({ error: 'storage_error' }, 500)\n }\n })\n\n return app\n}\n","import { createEntityRoute, createWorkspaceEntityRoute } from './entity-route'\nimport type { SnapshotStore } from '../db'\nimport type { MiddlewareHandler } from 'hono'\nimport type { AuthEnv } from '../middleware/auth'\n\n/**\n * RFC-108 D1:远端 memory 路由收口。\n *\n * 将原本分散在 /sessions/:wsKey/user:... 路径下的 memory 读写收口到\n * 专用路由 /memory/user/:resourceId,key = `user:<userId>:memory:<resourceId>`。\n * `resourceId` 对应 memory 文件相对路径的 URL-safe 编码(如 `AGENTS.md`/`MEMORY.md`/\n * `memory%2Ffoo.md`),支持同一用户下多个独立 memory 文件各自读写,不互相覆盖。\n */\nexport function createMemoryRoute(store: SnapshotStore, auth: MiddlewareHandler<AuthEnv>) {\n return createEntityRoute(store, auth, 'memory', '/memory/user')\n}\n\n/**\n * RFC-164 M164-2 D5:workspace 级 memory 路由——R 形态 opt-in 远端记忆同步。\n * 路由 /memory/workspace/:workspaceKey/:resourceId,\n * key = `workspace:<userId>:<workspaceKey>:memory:<resourceId>`(userId 仍是安全边界,\n * 见 `createWorkspaceEntityRoute` 注释)。`resourceId` 对应 auto-memory/lessons 文件相对\n * 路径的 URL-safe 编码(如 `MEMORY.md`/`memory%2Ffoo.md`/`lessons.json`)。\n */\nexport function createMemoryWorkspaceRoute(store: SnapshotStore, auth: MiddlewareHandler<AuthEnv>) {\n return createWorkspaceEntityRoute(store, auth, 'memory', '/memory/workspace')\n}\n","import { timingSafeEqual } from 'node:crypto'\nimport type { MiddlewareHandler } from 'hono'\nimport { deriveUserId } from '../db'\n\n/** auth 中间件向下游路由暴露的上下文变量。userId 由 token 服务端派生,不信客户端头。 */\nexport type AuthEnv = { Variables: { userId?: string } }\n\n/**\n * Bearer token 认证中间件。\n * token 为空字符串时不启用(开发环境)。\n *\n * RFC-074 R-SEC:鉴权通过后把**服务端从 token 派生的 userId**(SHA-256)写入上下文\n * (`c.set('userId', …)`),供下游路由作分区键——绝不信任客户端 `X-Otto-User-Id` 头(可伪造越权)。\n */\nexport function createAuthMiddleware(token: string): MiddlewareHandler<AuthEnv> {\n const enabled = token !== ''\n\n return async (c, next) => {\n if (!enabled) {\n return next()\n }\n\n const header = c.req.header('Authorization')\n\n if (!header) {\n return c.json({ error: 'unauthorized' }, 401)\n }\n\n const parts = header.split(' ')\n\n if (parts.length !== 2 || parts[0] !== 'Bearer') {\n return c.json({ error: 'forbidden' }, 403)\n }\n\n const supplied = parts[1]!\n\n const sa = new TextEncoder().encode(supplied)\n const sb = new TextEncoder().encode(token)\n\n if (sa.length !== sb.length || !timingSafeEqual(sa, sb)) {\n return c.json({ error: 'forbidden' }, 403)\n }\n\n // 身份绑定到已认证的 token,而非客户端头。\n c.set('userId', deriveUserId(supplied))\n return next()\n }\n}\n","import { Hono } from 'hono'\nimport { createStorageRoute } from './routes/storage'\nimport { createTraceRoute } from './routes/trace'\nimport { createAuthRoute } from './routes/auth'\nimport { createConfigRoute } from './routes/config'\nimport { createMemoryRoute, createMemoryWorkspaceRoute } from './routes/memory'\nimport { createAuthMiddleware } from './middleware/auth'\nimport type { ServerConfig } from './config'\nimport type { SnapshotStore, TraceLogStore } from './db'\nimport type { UserLoginResult } from './db'\n\nexport function createApp(\n store: SnapshotStore,\n traceStore: TraceLogStore,\n userLogin: { login: (token: string) => UserLoginResult },\n config: ServerConfig,\n): Hono {\n const app = new Hono()\n const auth = createAuthMiddleware(config.authToken)\n\n const storage = createStorageRoute(store, auth)\n app.route('/', storage)\n\n const trace = createTraceRoute(traceStore, auth)\n app.route('/', trace)\n\n const authRoute = createAuthRoute(config.authToken, userLogin)\n app.route('/', authRoute)\n\n const configRoute = createConfigRoute(store, auth)\n app.route('/', configRoute)\n\n const memoryRoute = createMemoryRoute(store, auth)\n app.route('/', memoryRoute)\n\n const memoryWorkspaceRoute = createMemoryWorkspaceRoute(store, auth)\n app.route('/', memoryWorkspaceRoute)\n\n return app\n}\n","/**\n * @x-otto/remote-persistence-server —— **极简 headless 存储守护进程**(`otto remote-persistence-server`)。\n *\n * RFC-067 M115-02(§7.3 现实修正,用户拍板「保留」):这**不是** `@x-otto/service`(`otto serve`)的\n * 冗余重写——两者是**两种产品形态**:\n * - 本包 = 零内部依赖、Hono + better-sqlite3 的极简 REST 存储后端,**精确对齐 RemotePersistence 线缆\n * 协议**(扁平 `GET/PUT/DELETE /sessions`、`/sessions/:id`、`/trace`、`/config/user`、`/auth/login`、\n * `/health` + Bearer)。服务 **M71 跨机器同步**特性(`otto --service <url>` / `/login --service` /\n * 按 user_id 分区的远端 session/memory/config)。适合 CI / 自托管 / headless 后端。\n * - `otto serve`(@x-otto/service)= 全功能 web-GUI 服务,**不同契约**(`/:namespace/:wsKey/:id` + WS + /api/*)。\n *\n * ⚠️ 勿当\"冗余/死代码\"删——它是活的 CLI 命令 + M71 同步特性的后端(1103 LOC,5 测试,零内部依赖)。\n * 删除前提=产品确认 M71 `--service` 跨机同步已废弃,且需整条栈协调删除(server + RemotePersistence\n * client + `--service` 路径),非孤立删本包。\n */\nimport { mkdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { serve } from '@hono/node-server'\nimport type { Server } from 'node:http'\nimport { loadConfig, REMOTE_PERSISTENCE_DEFAULT_PORT, type ServerConfig } from './config'\nimport {\n createSnapshotStore,\n createTraceLogStore,\n createUserLoginStore,\n type SnapshotStore,\n type TraceLogStore,\n type UserLoginResult,\n} from './db'\nimport { createApp } from './app'\nimport type { Hono } from 'hono'\n\nexport type { ServerConfig }\nexport type { SnapshotStore, TraceLogStore, UserLoginResult }\n\nexport { loadConfig, REMOTE_PERSISTENCE_DEFAULT_PORT, createSnapshotStore, createTraceLogStore, createUserLoginStore, createApp }\n\nexport interface StartServerResult {\n app: Hono\n server: Server\n url: string\n store: SnapshotStore\n traceStore: TraceLogStore\n}\n\nexport interface StartServerOptions {\n host?: string\n}\n\n/**\n * 创建并启动 remote persistence server。\n * 供 CLI 子命令和编程式嵌入使用。\n */\nexport function startServer(\n config: ServerConfig,\n opts: StartServerOptions = {},\n): Promise<StartServerResult> {\n const host = opts.host ?? '127.0.0.1'\n\n mkdirSync(config.dataDir, { recursive: true })\n const dbPath = join(config.dataDir, 'otto.db')\n const store = createSnapshotStore(dbPath)\n const traceStore = createTraceLogStore(dbPath)\n const userLogin = createUserLoginStore(dbPath)\n const app = createApp(store, traceStore, userLogin, config)\n\n const server = serve({ fetch: app.fetch, port: config.port, hostname: host }) as unknown as Server\n\n return new Promise((resolve, reject) => {\n server.once('listening', () => {\n const addr = server.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : config.port\n\n console.log(\n `[remote-persistence-server] listening on http://${host === '0.0.0.0' ? 'localhost' : host}:${actualPort}`,\n )\n console.log(\n `[remote-persistence-server] auth: ${config.authToken ? 'enabled' : 'DISABLED (dev only)'}`,\n )\n\n resolve({\n app,\n server,\n url: `http://${host === '0.0.0.0' ? 'localhost' : host}:${actualPort}`,\n store,\n traceStore,\n })\n })\n server.once('error', (err) => {\n store.close()\n reject(err)\n })\n })\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x-otto/remote-persistence-server",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org",
|
|
19
|
+
"tag": "alpha"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hono/node-server": "^1.19.0",
|
|
23
|
+
"better-sqlite3": "^12.9.0",
|
|
24
|
+
"hono": "^4.12.12"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@x-otto/persistence": "0.0.1-alpha.0",
|
|
28
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"private": false,
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"clean": "rm -rf dist"
|
|
38
|
+
}
|
|
39
|
+
}
|