@x-otto/remote-persistence-server 0.0.1-alpha.2 → 0.0.1-alpha.4
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 +32 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,47 +39,65 @@ No authentication required.
|
|
|
39
39
|
|
|
40
40
|
### Session Snapshot CRUD
|
|
41
41
|
|
|
42
|
-
All `/sessions/*` paths require an `Authorization: Bearer <AUTH_TOKEN>` header.
|
|
42
|
+
All `/sessions/*` paths require an `Authorization: Bearer <AUTH_TOKEN>` header. The `:wsKey` path
|
|
43
|
+
segment is accept-and-ignore in this single-tenant backend — real isolation is the token-derived
|
|
44
|
+
`userId` partition, not the client-supplied workspace key.
|
|
43
45
|
|
|
44
|
-
#### List
|
|
46
|
+
#### List Session IDs
|
|
45
47
|
|
|
46
48
|
```
|
|
47
|
-
GET /sessions
|
|
49
|
+
GET /sessions/:wsKey
|
|
50
|
+
Authorization: Bearer <token>
|
|
51
|
+
→ 200 PaginatedResult<string>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Returns a paginated result (`items/total/page/pageSize/totalPages/hasNext/hasPrevious`) of session
|
|
55
|
+
ids, ordered by `updated_at` descending. Supports `?page=` (0-based), `?pageSize=` (default 50) and
|
|
56
|
+
`?order=asc|desc`.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
GET /sessions/:wsKey/recent
|
|
48
60
|
Authorization: Bearer <token>
|
|
49
61
|
→ 200 { "ids": ["session-1", "session-2"] }
|
|
50
62
|
```
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
Lightweight recent-ids endpoint, ordered by `metadata.lastActiveAt` descending, excluding archived
|
|
65
|
+
entries. Supports `?limit=` (default 50, max 1000).
|
|
53
66
|
|
|
54
67
|
#### Get a Single Snapshot
|
|
55
68
|
|
|
56
69
|
```
|
|
57
|
-
GET /sessions/:id
|
|
70
|
+
GET /sessions/:wsKey/:id
|
|
58
71
|
Authorization: Bearer <token>
|
|
59
|
-
→ 200 <Snapshot JSON>
|
|
72
|
+
→ 200 <Snapshot JSON> (ETag header = current revision)
|
|
60
73
|
→ 404 { "error": "not_found" }
|
|
61
74
|
```
|
|
62
75
|
|
|
63
|
-
Returns the full JSON as sent by the client's `PUT`.
|
|
76
|
+
Returns the full JSON as sent by the client's `PUT`. The `ETag` response header carries the current
|
|
77
|
+
revision for conditional (CAS) writes.
|
|
64
78
|
|
|
65
79
|
#### Save/Update a Snapshot
|
|
66
80
|
|
|
67
81
|
```
|
|
68
|
-
PUT /sessions/:id
|
|
82
|
+
PUT /sessions/:wsKey/:id
|
|
69
83
|
Authorization: Bearer <token>
|
|
70
84
|
Content-Type: application/json
|
|
85
|
+
If-Match: <revision> # optional: CAS guard
|
|
71
86
|
|
|
72
87
|
<Snapshot JSON>
|
|
73
|
-
→ 200 { "ok": true }
|
|
74
|
-
→ 400 { "error": "invalid_body" }
|
|
88
|
+
→ 200 { "ok": true } (ETag header = new revision)
|
|
89
|
+
→ 400 { "error": "invalid_body" | "invalid_if_match" }
|
|
90
|
+
→ 403 { "error": "forbidden" } # user-key mismatch
|
|
91
|
+
→ 409 { "error": "conflict", "revision": <current> }
|
|
75
92
|
```
|
|
76
93
|
|
|
77
|
-
Idempotent — a repeated `PUT` overwrites with the new data.
|
|
94
|
+
Idempotent — a repeated `PUT` overwrites with the new data. Without `If-Match` the write is
|
|
95
|
+
unconditional; with `If-Match` a revision mismatch returns `409` instead of overwriting.
|
|
78
96
|
|
|
79
97
|
#### Delete a Snapshot
|
|
80
98
|
|
|
81
99
|
```
|
|
82
|
-
DELETE /sessions/:id
|
|
100
|
+
DELETE /sessions/:wsKey/:id
|
|
83
101
|
Authorization: Bearer <token>
|
|
84
102
|
→ 200 { "ok": true }
|
|
85
103
|
→ 404 { "error": "not_found" }
|
|
@@ -184,6 +202,8 @@ The SQLite database file lives at `<DATA_DIR>/otto.db`, WAL mode, with the follo
|
|
|
184
202
|
CREATE TABLE snapshots (
|
|
185
203
|
id TEXT PRIMARY KEY,
|
|
186
204
|
data TEXT NOT NULL, -- the full Snapshot JSON
|
|
205
|
+
user_id TEXT, -- token-derived partition (SHA-256 of Bearer token)
|
|
206
|
+
revision INTEGER NOT NULL DEFAULT 1, -- CAS revision, exposed via ETag / If-Match
|
|
187
207
|
created_at INTEGER NOT NULL,
|
|
188
208
|
updated_at INTEGER NOT NULL
|
|
189
209
|
);
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ interface TraceLogStore {
|
|
|
66
66
|
truncate(userId: string, sessionId: string, toSeq: number): void;
|
|
67
67
|
clear(userId: string, sessionId: string): void;
|
|
68
68
|
listSessions(userId: string): string[];
|
|
69
|
+
close(): void;
|
|
69
70
|
}
|
|
70
71
|
declare function createSnapshotStore(dbPath: string): SnapshotStore;
|
|
71
72
|
declare function createTraceLogStore(dbPath: string): TraceLogStore;
|
|
@@ -100,6 +101,9 @@ interface StartServerResult {
|
|
|
100
101
|
url: string;
|
|
101
102
|
store: SnapshotStore;
|
|
102
103
|
traceStore: TraceLogStore;
|
|
104
|
+
userLogin: ReturnType<typeof createUserLoginStore>;
|
|
105
|
+
/** 统一关停:先关 HTTP server,再关三个 SQLite 连接。 */
|
|
106
|
+
close(): void;
|
|
103
107
|
}
|
|
104
108
|
interface StartServerOptions {
|
|
105
109
|
host?: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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;EACb,KAAA;AAAA;AAAA,iBAGc,mBAAA,CAAoB,MAAA,WAAiB,aAAA;AAAA,iBAyIrC,mBAAA,CAAoB,MAAA,WAAiB,aAAA;;;;;UA8GpC,eAAA;EACf,MAAA;EACA,KAAA;AAAA;;;;;;;iBASc,oBAAA,CAAqB,MAAA;EACnC,KAAA,GAAQ,KAAA,aAAkB,eAAA;EAC1B,KAAA;AAAA;;;iBCpUc,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;EACZ,SAAA,EAAW,UAAA,QAAkB,oBAAA;;EAE7B,KAAA;AAAA;AAAA,UAGe,kBAAA;EACf,IAAA;AAAA;;;;;iBAOc,WAAA,CACd,MAAA,EAAQ,YAAA,EACR,IAAA,GAAM,kBAAA,GACL,OAAA,CAAQ,iBAAA"}
|
package/dist/index.js
CHANGED
|
@@ -39,11 +39,11 @@ CREATE INDEX IF NOT EXISTS idx_snapshots_updated_at
|
|
|
39
39
|
`);let r={append:t.prepare(`
|
|
40
40
|
INSERT INTO trace_events (user_id, session_id, seq, data, ts)
|
|
41
41
|
VALUES (?, ?, (SELECT COALESCE(MAX(seq), -1) + 1 FROM trace_events WHERE user_id = ? AND session_id = ?), ?, ?)
|
|
42
|
-
`),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`)},i=t.transaction((e,t,n,i)=>(r.append.run(e,t,e,t,n,i),r.lastSeq.get(e,t).seq));return{append(e,t,n){return i(e,t,n,Date.now())},read(e,t,n,i){let a=n??0,o=i??2**53-1;return r.read.all(e,t,a,o).map(e=>e.data)},count(e,t){return r.count.get(e,t).n},truncate(e,t,n){r.truncate.run(e,t,n)},clear(e,t){r.clear.run(e,t)},listSessions(e){return r.listSessions.all(e).map(e=>e.session_id)}}}function y(e){let t=new h(e);t.pragma(`journal_mode = WAL`),t.exec(`
|
|
42
|
+
`),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`)},i=t.transaction((e,t,n,i)=>(r.append.run(e,t,e,t,n,i),r.lastSeq.get(e,t).seq));return{append(e,t,n){return i(e,t,n,Date.now())},read(e,t,n,i){let a=n??0,o=i??2**53-1;return r.read.all(e,t,a,o).map(e=>e.data)},count(e,t){return r.count.get(e,t).n},truncate(e,t,n){r.truncate.run(e,t,n)},clear(e,t){r.clear.run(e,t)},listSessions(e){return r.listSessions.all(e).map(e=>e.session_id)},close(){t.close()}}}function y(e){let t=new h(e);t.pragma(`journal_mode = WAL`),t.exec(`
|
|
43
43
|
CREATE TABLE IF NOT EXISTS users (
|
|
44
44
|
user_id TEXT PRIMARY KEY,
|
|
45
45
|
token_hash TEXT NOT NULL,
|
|
46
46
|
created_at INTEGER NOT NULL
|
|
47
47
|
);
|
|
48
|
-
`);let n=g,r=t.prepare(`INSERT OR IGNORE INTO users (user_id, token_hash, created_at) VALUES (?, ?, ?)`),i=t.prepare(`SELECT user_id, token_hash FROM users WHERE token_hash = ?`);return{login(e){let t=n(e),a=Date.now();return r.run(t,t,a),{userId:i.get(t)?.user_id??t,token:e}},close(){t.close()}}}function b(e,t){return e.startsWith(`user:`)?t?e.split(`:`)[1]===t:!1:!0}function x(e,t){let n=new s;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),i=Number.parseInt(t.req.query(`page`)??``,10),a=Number.parseInt(t.req.query(`pageSize`)??``,10),o=Number.isFinite(i)&&i>=0?i:0,s=Number.isFinite(a)&&a>0?a:50,c=(t.req.query(`order`)===`asc`?`asc`:`desc`)==`asc`?[...r].reverse():r;return t.json(d(c,{page:o,pageSize:s}))}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(!b(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(!b(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!b(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 S(e,t){let n=new s;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 C(e,t){let n=new s;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 w(e,t){let n=new s;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 T(e,t,n,r){let i=new s,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,o=`${r}/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t)=>`user:${e}:${n}:${t}`;return i.get(o,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(o,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 E(e,t,n,r){let i=new s,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,o=`${r}/:workspaceKey/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t,r)=>`workspace:${e}:${t}:${n}:${r}`;return i.get(o,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(o,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 D(e,t){return T(e,t,`memory`,`/memory/user`)}function O(e,t){return E(e,t,`memory`,`/memory/workspace`)}function k(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),l=new TextEncoder().encode(e);return c.length!==l.length||!a(c,l)?n.json({error:`forbidden`},403):(n.set(`userId`,g(s)),r())}}function A(e,t,n,r){let i=new s,a=k(r.authToken),o=x(e,a);i.route(`/`,o);let c=S(t,a);i.route(`/`,c);let l=C(r.authToken,n);i.route(`/`,l);let u=w(e,a);i.route(`/`,u);let d=D(e,a);i.route(`/`,d);let f=O(e,a);return i.route(`/`,f),i}function j(e,i={}){let a=i.host??`127.0.0.1`;t(e.dataDir,{recursive:!0});let o=n(e.dataDir,`otto.db`),s=_(o),c=v(o),l=A(s,c,
|
|
48
|
+
`);let n=g,r=t.prepare(`INSERT OR IGNORE INTO users (user_id, token_hash, created_at) VALUES (?, ?, ?)`),i=t.prepare(`SELECT user_id, token_hash FROM users WHERE token_hash = ?`);return{login(e){let t=n(e),a=Date.now();return r.run(t,t,a),{userId:i.get(t)?.user_id??t,token:e}},close(){t.close()}}}function b(e,t){return e.startsWith(`user:`)?t?e.split(`:`)[1]===t:!1:!0}function x(e,t){let n=new s;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),i=Number.parseInt(t.req.query(`page`)??``,10),a=Number.parseInt(t.req.query(`pageSize`)??``,10),o=Number.isFinite(i)&&i>=0?i:0,s=Number.isFinite(a)&&a>0?a:50,c=(t.req.query(`order`)===`asc`?`asc`:`desc`)==`asc`?[...r].reverse():r;return t.json(d(c,{page:o,pageSize:s}))}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(!b(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(!b(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!b(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 S(e,t){let n=new s;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 C(e,t){let n=new s;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 w(e,t){let n=new s;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 T(e,t,n,r){let i=new s,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,o=`${r}/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t)=>`user:${e}:${n}:${t}`;return i.get(o,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(o,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 E(e,t,n,r){let i=new s,a=r.split(`/`).slice(0,-1).join(`/`)||`/`,o=`${r}/:workspaceKey/:resourceId`;i.use(`${a}/*`,t),i.use(`${r}/*`,t);let c=(e,t,r)=>`workspace:${e}:${t}:${n}:${r}`;return i.get(o,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(o,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 D(e,t){return T(e,t,`memory`,`/memory/user`)}function O(e,t){return E(e,t,`memory`,`/memory/workspace`)}function k(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),l=new TextEncoder().encode(e);return c.length!==l.length||!a(c,l)?n.json({error:`forbidden`},403):(n.set(`userId`,g(s)),r())}}function A(e,t,n,r){let i=new s,a=k(r.authToken),o=x(e,a);i.route(`/`,o);let c=S(t,a);i.route(`/`,c);let l=C(r.authToken,n);i.route(`/`,l);let u=w(e,a);i.route(`/`,u);let d=D(e,a);i.route(`/`,d);let f=O(e,a);return i.route(`/`,f),i}function j(e,i={}){let a=i.host??`127.0.0.1`;t(e.dataDir,{recursive:!0});let o=n(e.dataDir,`otto.db`),s=_(o),c=v(o),l=y(o),u=A(s,c,l,e),d=r({fetch:u.fetch,port:e.port,hostname:a});return new Promise((t,n)=>{d.once(`listening`,()=>{let n=d.address(),r=typeof n==`object`&&n?n.port:e.port;console.log(`[remote-persistence-server] listening on http://${a===`0.0.0.0`?`localhost`:a}:${r}`),console.log(`[remote-persistence-server] auth: ${e.authToken?`enabled`:`DISABLED (dev only)`}`),t({app:u,server:d,url:`http://${a===`0.0.0.0`?`localhost`:a}:${r}`,store:s,traceStore:c,userLogin:l,close(){d.close(),s.close(),c.close(),l.close()}})}),d.once(`error`,e=>{s.close(),c.close(),l.close(),n(e)})})}export{c as REMOTE_PERSISTENCE_DEFAULT_PORT,A as createApp,_ as createSnapshotStore,v as createTraceLogStore,y as createUserLoginStore,l as loadConfig,j as startServer};
|
|
49
49
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["f","u","Database","paginate"],"sources":["../src/config.ts","../../persistence/dist/index.js","../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{dirname as e,join as t}from\"node:path\";import{mkdir as n,open as r,readFile as i,readdir as a,rename as o,stat as s,unlink as c}from\"node:fs/promises\";import{randomUUID as l}from\"node:crypto\";import{constants as u,mkdirSync as d}from\"node:fs\";import{DatabaseSync as f}from\"node:sqlite\";import{StringDecoder as p}from\"node:string_decoder\";var m=class extends Error{constructor(e,t){super(e,t),this.name=`PersistenceError`}},h=class extends m{statusCode;constructor(e,t,n){super(e,n),this.name=`RemotePersistenceError`,this.statusCode=t}},g=class extends h{info;constructor(e,t,n){super(e,409,n),this.name=`RemoteSnapshotConflictError`,this.info=t}};async function _(t,n){let i=`${t}.${l()}.tmp`;try{let a=await r(i,`w`);try{await a.writeFile(n),await a.sync()}finally{await a.close()}await o(i,t),await ee(e(t))}catch(e){throw await c(i).catch(()=>{}),e instanceof m?e:new m(`Failed to atomically write \"${t}\"`,{cause:e})}}async function ee(e){let t;try{t=await r(e,`r`),await t.sync()}catch{}finally{t&&await t.close().catch(()=>{})}}function v(e,t){let n=t.pageSize>=1?t.pageSize:1,r=Math.max(1,Math.ceil(e/n)),i=Math.max(0,Math.min(t.page,r-1));return{total:e,page:i,pageSize:n,totalPages:r,start:i*n,hasNext:i<r-1,hasPrevious:i>0}}function y(e,t){let n=v(e.length,t);return{items:e.slice(n.start,n.start+n.pageSize),total:n.total,page:n.page,pageSize:n.pageSize,totalPages:n.totalPages,hasNext:n.hasNext,hasPrevious:n.hasPrevious}}var b=class{store=new Map;defaultPageSize;defaultSortBy;constructor(e={}){this.defaultPageSize=e.defaultPageSize??20,this.defaultSortBy=e.defaultSortBy??`updatedAt`}getSortValue(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}async save(e){this.store.set(this.extractId(e),structuredClone(e))}async load(e){let t=this.store.get(e);return t?structuredClone(t):null}async delete(e){return this.store.delete(e)}async list(){return Array.from(this.store.keys())}entries(){return Array.from(this.store.entries())}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??this.defaultSortBy,i=e.pageSize??this.defaultPageSize,a=Array.from(this.store.entries()).map(([e,t])=>({id:e,sortValue:this.getSortValue(t,r)}));return a.sort((e,t)=>n===`asc`?e.sortValue-t.sortValue:t.sortValue-e.sortValue),y(a.map(e=>e.id),{page:t,pageSize:i})}},x=class extends b{extractId(e){return e.id}};async function S(e,t,n,r){let{now:i,version:a=1,tags:o=[],preserveCreatedAt:s=!0,owner:c,updatedBy:l}=r,u=s?await e.load(t):null,d=u?.metadata?.owner;await e.save({version:a,id:t,data:n,metadata:{createdAt:u?.metadata.createdAt??i,updatedAt:i,tags:o,...d?{owner:d}:c?{owner:c}:{},...l?{updatedBy:l}:{}}})}var C=class{baseDir;extension;defaultPageSize;codec;initialized=!1;constructor(e){this.baseDir=e.baseDir,this.extension=e.extension??`.json`,this.defaultPageSize=e.defaultPageSize??20,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`}}async save(e){await this.ensureDir(),await _(this.resolvePath(this.extractId(e)),this.codec.encode(e))}async load(e){await this.ensureDir();let t=this.resolvePath(e);try{let e=await i(t,`utf-8`);return this.codec.decode(e)}catch(t){if(w(t))return null;throw new m(`Failed to load \"${e}\"`,{cause:t})}}async delete(e){try{return await c(this.resolvePath(e)),!0}catch{return!1}}async list(){await this.ensureDir();try{return(await a(this.baseDir)).filter(e=>e.endsWith(this.extension)).map(e=>decodeURIComponent(e.slice(0,-this.extension.length)))}catch{return[]}}async listPaginated(e){await this.ensureDir();let{page:n,order:r=`desc`}=e,i=e.pageSize??this.defaultPageSize;try{let e=(await a(this.baseDir)).filter(e=>e.endsWith(this.extension)),o=await Promise.all(e.map(async e=>{let n=await s(t(this.baseDir,e));return{id:decodeURIComponent(e.slice(0,-this.extension.length)),mtime:n.mtimeMs}}));return o.sort((e,t)=>r===`asc`?e.mtime-t.mtime:t.mtime-e.mtime),y(o.map(e=>e.id),{page:n,pageSize:i})}catch{return{items:[],total:0,page:0,pageSize:i,totalPages:1,hasNext:!1,hasPrevious:!1}}}resolvePath(e){let n=encodeURIComponent(e);return t(this.baseDir,`${n}${this.extension}`)}async ensureDir(){this.initialized||=(await n(this.baseDir,{recursive:!0}),!0)}};function w(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}var T=class{map=new Map;constructor(e){this.extractId=e}async save(e){this.map.set(this.extractId(e),e)}async load(e){return this.map.get(e)??null}async delete(e){return this.map.delete(e)}},E=class extends C{extractId(e){return e.sessionId}},te=class{store;constructor(e){this.store=new E({baseDir:e.baseDir,extension:`.json`})}async save(e){return this.store.save(e)}async load(e){return this.store.load(e)}async delete(e){return this.store.delete(e)}};function ne(){return new T(e=>e.sessionId)}var D=class extends C{extractId(e){return e.sessionId}},re=class{store;constructor(e){this.store=new D({baseDir:e.baseDir,extension:`.json`})}async save(e){return this.store.save(e)}async load(e){return this.store.load(e)}async delete(e){return this.store.delete(e)}};function O(e){let t=typeof e==`object`&&e?e:{},n=typeof t.nextId==`number`&&Number.isFinite(t.nextId)?Math.floor(t.nextId):0,r=Math.max(1,n),i=Array.isArray(t.entries)?t.entries:[];for(let e of i){let t=e?.id;typeof t==`number`&&Number.isFinite(t)&&(r=Math.max(r,Math.floor(t)+1))}return r}function k(e,t){let n=typeof t==`object`&&t?t:{},r=Array.isArray(n.entries)?n.entries:[],i=[],a=new Set;for(let e of r){if(typeof e!=`object`||!e)continue;let{id:t,text:n}=e;if(typeof t!=`number`||!Number.isFinite(t)||typeof n!=`string`)continue;let r=Math.floor(t);a.has(r)||(a.add(r),i.push({id:r,text:n,byteLength:Buffer.byteLength(n,`utf-8`)}))}return{sessionId:e,entries:i,nextId:O(n)}}function A(){return new T(e=>e.sessionId)}var ie=class{baseUrl;resolveWsKey;get wsKey(){return this.resolveWsKey()}getAuth;getHeaders;fetch;timeoutMs;defaultPageSize;defaultSortBy;codec;get serviceLabel(){return`远端持久化服务`}get endpointFlag(){return`--persistence-url`}constructor(e){this.baseUrl=e.baseUrl.replace(/\\/+$/,``);let t=e.wsKey;this.resolveWsKey=typeof t==`function`?()=>t()||`default`:()=>t||`default`,this.getAuth=e.getAuth,this.getHeaders=e.getHeaders??(async()=>({})),this.fetch=e.fetch,this.timeoutMs=e.timeoutMs,this.defaultPageSize=e.defaultPageSize??20,this.defaultSortBy=e.defaultSortBy??`updatedAt`,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`}}wsPath(e){return`/${encodeURIComponent(this.wsKey)}/${encodeURIComponent(e)}`}wsListPath(){return`/${encodeURIComponent(this.wsKey)}`}async save(e){await this.request(this.wsPath(this.extractId(e)),{method:`PUT`,body:this.codec.encode(e)})}async saveWithRevision(e,t){let n={};t!==void 0&&(n[`If-Match`]=String(t));let r=(await this.request(this.wsPath(this.extractId(e)),{method:`PUT`,body:this.codec.encode(e)},n)).headers.get(`ETag`);return r?Number(r.replace(/\"/g,``)):NaN}async loadWithRevision(e){let t=await this.request(this.wsPath(e),{method:`GET`});if(t.status===404)return null;let n=await t.text(),r;try{r=this.codec.decode(n)}catch(n){throw new h(`Remote persistence decode failed: ${this.wsPath(e)}`,t.status,{cause:n})}let i=t.headers.get(`ETag`),a=i?Number(i.replace(/\"/g,``)):0;return{snapshot:r,revision:a}}async load(e){return(await this.loadWithRevision(e))?.snapshot??null}async delete(e){return(await this.request(this.wsPath(e),{method:`DELETE`})).ok}async list(){let e=await(await this.request(this.wsListPath(),{method:`GET`})).json();return e.items??e.ids??[]}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??this.defaultSortBy,i=e.pageSize??this.defaultPageSize,a=new URLSearchParams({page:String(t),pageSize:String(i),sortBy:r,order:n});return(await this.request(`${this.wsListPath()}?${a.toString()}`,{method:`GET`})).json()}async request(e,t,n){let r=await this.getAuth(),i={...await this.getHeaders(),...r.token?{Authorization:`Bearer ${r.token}`}:{},Accept:`application/json`,...n};t.body&&(i[`Content-Type`]=this.codec.contentType??`application/json`);let a=new AbortController,o=setTimeout(()=>a.abort(),this.timeoutMs),s=`${this.baseUrl}${e}`;try{let n=await this.fetch(s,{method:t.method,headers:i,body:t.body,signal:a.signal});if(n.status===409){let t=null;try{let e=await n.clone().json();t=typeof e.revision==`number`?e.revision:null}catch{}throw new g(`Remote persistence CAS conflict: ${this.wsPath(e.split(`/`).pop()??``)}`,{revision:t})}if(!n.ok&&n.status!==404)throw new h(`Remote persistence API error: ${n.status} ${n.statusText}`,n.status);return n}catch(e){if(e instanceof g||e instanceof h)throw e;if(e instanceof DOMException&&e.name===`AbortError`)throw new h(`无法连接到${this.serviceLabel}(请求超时 ${this.timeoutMs/1e3}s):${t.method} ${s}\\n提示:请确认${this.serviceLabel}是否已启动、${this.endpointFlag} 地址与端口是否正确。`,0,{cause:e});let n=e?.cause?.code,r=n?n===`ECONNREFUSED`?`(连接被拒绝)`:n===`ENOTFOUND`?`(DNS 解析失败,地址不存在)`:n===`ECONNRESET`?`(连接被重置)`:n===`ETIMEDOUT`?`(连接超时)`:n===`CERT_HAS_EXPIRED`?`(TLS 证书已过期)`:n===`DEPTH_ZERO_SELF_SIGNED_CERT`?`(自签名证书被拒)`:`(${n})`:``,i=e instanceof Error?e.message:String(e);throw new h(`无法连接到${this.serviceLabel}:${t.method} ${s}\\n原因:${i}${r}\\n提示:请确认${this.serviceLabel}是否已启动、${this.endpointFlag} 地址与端口是否正确。`,0,{cause:e})}finally{clearTimeout(o)}}},j=class extends C{constructor(e){super({baseDir:e.baseDir,extension:e.extension,codec:e.codec})}extractId(e){return e.id}};const ae=process.emitWarning;process.emitWarning=function(e,...t){if(!(typeof e==`string`&&e.includes(`SQLite is an experimental feature`)))return ae.call(process,e,...t)};function oe(e){let t=e=>e.map(e=>e===void 0?null:e);return{run(...n){return e.run(...t(n))},get(...n){return e.get(...t(n))},all(...n){return e.all(...t(n))},setReadBigInts(t){e.setReadBigInts(t)}}}let M=0;var N=class{db;name;constructor(e,t){this.db=t?new f(e,t):new f(e),this.name=e}prepare(e){return oe(this.db.prepare(e))}exec(e){this.db.exec(e)}close(){this.db.close()}pragma(e,t){if(e.includes(`=`)){this.db.exec(`PRAGMA ${e}`);return}let n=this.db.prepare(`PRAGMA ${e}`).all();if(t?.simple){if(n.length===0)return;let e=n[0];return Object.values(e)[0]}return n}transaction(e){let t=t=>(...n)=>{let r=`sp_${M}`;M++===0?this.db.exec(t?`BEGIN IMMEDIATE`:`BEGIN`):this.db.exec(`SAVEPOINT ${r}`);try{let t=e(...n);return--M===0?this.db.exec(`COMMIT`):this.db.exec(`RELEASE ${r}`),t}catch(e){throw--M===0?this.db.exec(`ROLLBACK`):(this.db.exec(`ROLLBACK TO ${r}`),this.db.exec(`RELEASE ${r}`)),e}},n=t(!1);return n.immediate=t(!0),n}};const P=new Map,F=[{version:1,up:e=>{e.exec(`\n -- snapshots (SqlitePersistence)\n CREATE TABLE IF NOT EXISTS snapshots (\n ns TEXT NOT NULL,\n id TEXT NOT NULL,\n version INTEGER NOT NULL DEFAULT 1,\n data TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n tags TEXT NOT NULL DEFAULT '[]',\n PRIMARY KEY (ns, id)\n );\n CREATE INDEX IF NOT EXISTS idx_snapshots_ns_updated\n ON snapshots(ns, updated_at DESC);\n CREATE INDEX IF NOT EXISTS idx_snapshots_ns_created\n ON snapshots(ns, created_at DESC);\n\n -- change_log (SqlitePersistence)\n CREATE TABLE IF NOT EXISTS change_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n op TEXT NOT NULL,\n ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_change_log_ns_ts\n ON change_log(ns, ts);\n `)}},{version:2,up:e=>{e.exec(`\n -- append_log(RFC-089 M28 事件溯源日志表结构;消费实现 SqliteAppendLog 已删除——\n -- 终局架构 review 2026-07-10 P1-7 确认零生产接线,迁移本身保留不动以维持已发布\n -- migration 历史的不可变性,仅此注释更新去掉对已删除类名的引用)\n CREATE TABLE IF NOT EXISTS append_log (\n stream_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n payload TEXT NOT NULL,\n PRIMARY KEY (stream_id, seq)\n );\n `)}}],I=new Set;function L(e){return e.reduce((e,t)=>Math.max(e,t.version),0)}function R(e,t,n){let r=L(n);if(t===`:memory:`){if(e.pragma(`user_version`,{simple:!0})===0){for(let t of n)t.up(e);e.pragma(`user_version = ${r}`)}return}if(I.has(t))return;let i=e.pragma(`user_version`,{simple:!0});if(i===0)for(let t of n)t.up(e);else if(i<r)for(let t of n)t.version>i&&t.up(e);else if(i>r)throw Error(`DB at ${t} has schema v${i} > code v${r}. Downgrade not supported. Upgrade otto first.`);e.pragma(`user_version = ${r}`),I.add(t)}function z(t,n=!0,r=F){if(t===`:memory:`){let e=new N(`:memory:`);return R(e,t,r),e}let i=P.get(t);if(i){let e=L(r);if(i.migrationTarget!==e)throw Error(`acquireDb cache conflict at ${t}: cached schema v${i.migrationTarget} but requested migrationSet targets v${e}. 同一 db 路径不可注入不同 migration 段。`);return i.refs++,i.db}d(e(t),{recursive:!0});let a=new N(t);return n&&(a.pragma(`journal_mode = WAL`),a.pragma(`synchronous = NORMAL`),a.pragma(`busy_timeout = 5000`)),R(a,t,r),P.set(t,{db:a,refs:1,migrationTarget:L(r)}),a}function B(e){if(e.name===`:memory:`){e.close();return}let t=e.name,n=P.get(t);n&&(n.refs--,n.refs<=0&&(e.close(),P.delete(t)))}var V=class{db;ns;defaultPageSize;codec;constructor(e){this.ns=e.namespace,this.defaultPageSize=e.defaultPageSize??20,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`},this.db=z(e.dbPath,e.wal!==!1,F)}close(){B(this.db)}async save(e){let t=e.id,n=e.version??1,r=this.codec.encode(e),i=e.metadata?.updatedAt??Date.now(),a=e.metadata?.createdAt??i,o=JSON.stringify(e.metadata?.tags??[]),s=this.db.prepare(`\n INSERT INTO snapshots (ns, id, version, data, created_at, updated_at, tags)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (ns, id) DO UPDATE SET\n version = excluded.version,\n data = excluded.data,\n updated_at = excluded.updated_at,\n tags = excluded.tags\n `),c=this.db.prepare(`INSERT INTO change_log (ns, doc_id, op, ts) VALUES (?, ?, 'save', ?)`);this.db.transaction(()=>{s.run(this.ns,t,n,r,a,i,o),c.run(this.ns,t,i)})()}async load(e){let t=this.db.prepare(`SELECT data FROM snapshots WHERE ns = ? AND id = ?`).get(this.ns,e);return t?this.codec.decode(t.data):null}async delete(e){let t=this.db.prepare(`DELETE FROM snapshots WHERE ns = ? AND id = ?`),n=this.db.prepare(`INSERT INTO change_log (ns, doc_id, op, ts) VALUES (?, ?, 'delete', ?)`);return this.db.transaction(()=>t.run(this.ns,e).changes>0?(n.run(this.ns,e,Date.now()),!0):!1)()}async list(){return this.db.prepare(`SELECT id FROM snapshots WHERE ns = ? ORDER BY updated_at DESC`).all(this.ns).map(e=>e.id)}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??`updated_at`,i=e.pageSize??this.defaultPageSize,a=r===`created_at`?`created_at`:`updated_at`,o=n===`asc`?`ASC`:`DESC`,{count:s}=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots WHERE ns = ?`).get(this.ns),c=v(s,{page:t,pageSize:i}),l=c.start;return{items:this.db.prepare(`SELECT id FROM snapshots WHERE ns = ? ORDER BY ${a} ${o} LIMIT ? OFFSET ?`).all(this.ns,c.pageSize,l).map(e=>e.id),total:c.total,page:c.page,pageSize:c.pageSize,totalPages:c.totalPages,hasNext:c.hasNext,hasPrevious:c.hasPrevious}}getChangeLog(e,t,n=100){return this.db.prepare(`SELECT id, ns, doc_id, op, ts FROM change_log WHERE ns = ? AND ts >= ? ORDER BY ts ASC, id ASC LIMIT ?`).all(e,t,n)}getChangesSince(e,t,n){let r=`SELECT id, updated_at FROM snapshots WHERE ns = ? AND updated_at > ?`,i=[e,t];n&&n.length>0&&(r+=` AND id IN (${n.map(()=>`?`).join(`,`)})`,i.push(...n)),r+=` ORDER BY updated_at ASC`;let a=this.db.prepare(r).all(...i),o=a.length>0?a.reduce((e,t)=>Math.max(e,t.updated_at),0):this.getLatestCursor(e);return{ids:a.map(e=>e.id),cursor:o}}getDeletedSince(e,t){return this.db.prepare(`SELECT doc_id FROM change_log WHERE ns = ? AND op = 'delete' AND ts > ?`).all(e,t).map(e=>e.doc_id)}getLatestCursor(e){return this.db.prepare(`SELECT MAX(updated_at) as ts FROM snapshots WHERE ns = ?`).get(e)?.ts??0}getLatestChangeTs(e){return this.db.prepare(`SELECT MAX(ts) as ts FROM change_log WHERE ns = ?`).get(e)?.ts??0}getInfo(){let e=this.db.pragma(`journal_mode`),[{count:t}]=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots`).all(),[{count:n}]=this.db.prepare(`SELECT COUNT(*) as count FROM change_log`).all(),[{count:r}]=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots WHERE ns = 'sessions'`).all(),i=this.db.pragma(`integrity_check`),a=i.length===1&&i[0]?.integrity_check===`ok`;return{walEnabled:e===`wal`,snapshots:t,changeLogEntries:n,sessions:r,integrity:a?`ok`:`fail: ${JSON.stringify(i)}`}}};function H(e){switch(e.type){case`memory`:return new x;case`file`:return new j({baseDir:e.baseDir,extension:e.extension,codec:e.codec});case`sqlite`:return new V({dbPath:e.dbPath,wal:e.wal,namespace:e.namespace,defaultPageSize:e.defaultPageSize,codec:e.codec});default:throw Error(`Unsupported storage type: ${e.type}`)}}const U=u.O_DSYNC==null?u.O_WRONLY|u.O_CREAT|u.O_APPEND:u.O_WRONLY|u.O_CREAT|u.O_APPEND|u.O_DSYNC,W=1024*1024,G=1e3,K=1e4;function q(e,t){e.anchors.push(t),e.anchors.length>K&&e.anchors.splice(0,Math.ceil(K*.1))}function se(e,t){let n=t?.from??0,r=t?.to??1/0;return e>=n&&e<r}var J=class{streams=new Map;listeners=new Map;maxRecordsPerStream;onEvict;constructor(e){this.maxRecordsPerStream=e?.maxRecordsPerStream??1e4,this.onEvict=e?.onEvict}getOrCreateStream(e){let t=this.streams.get(e);return t||(t={records:[],nextSeq:0,droppedCount:0},this.streams.set(e,t)),t}async append(e,t){let n=this.getOrCreateStream(e),r={seq:n.nextSeq,streamId:e,entry:structuredClone(t)};if(n.nextSeq++,n.records.push(r),n.records.length>this.maxRecordsPerStream){let t=n.records.shift();n.droppedCount++,this.onEvict?.(e,t.seq,n.droppedCount)}let i=this.listeners.get(e);if(i)for(let e of i)e(structuredClone(r));return structuredClone(r)}async*read(e,t){let n=this.streams.get(e)?.records??[];for(let e of n)se(e.seq,t)&&(yield structuredClone(e))}async count(e){return this.streams.get(e)?.nextSeq??0}async*tail(e){let t=[],n=null,r=e=>{t.push(e),n&&=(n(),null)},i=this.listeners.get(e)??new Set;i.add(r),this.listeners.set(e,i);let a=-1;try{for(let t of this.streams.get(e)?.records??[])yield structuredClone(t),a=t.seq;for(;;){t.length===0&&await new Promise(e=>{n=e});for(let e of t.splice(0,t.length))e.seq>a&&(yield e,a=e.seq)}}finally{i.delete(r)}}async truncate(e,t){let n=this.streams.get(e);if(!n)return;if(t<=0){this.streams.delete(e);return}let r=n.records.findIndex(e=>e.seq>=t);r!==-1&&(n.records.length=r,n.nextSeq=t)}async clear(e){this.streams.delete(e)}},Y=class{baseDir;extension;codec;pollIntervalMs;seqCounters=new Map;byteIndexes=new Map;handles=new Map;repairedStreams=new Set;writeQueue=Promise.resolve();dirReady=!1;closed=!1;constructor(e){this.baseDir=e.baseDir,this.extension=e.extension??`.jsonl`,this.pollIntervalMs=e.pollIntervalMs??50,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e)}}append(e,t){if(this.closed)return Promise.resolve({seq:-1,streamId:e,entry:t});let n=this.writeQueue.then(()=>this.appendSerial(e,t));return this.writeQueue=n.catch(()=>void 0),n}async appendSerial(e,t){await this.ensureDir(),this.repairedStreams.has(e)||(await this.repairPartialTail(e),this.repairedStreams.add(e));let n=await this.nextSeq(e),r={seq:n,streamId:e,entry:t},i=`${this.codec.encode(t)}\\n`,a=Buffer.byteLength(i,`utf-8`),o=this.handles.get(e);try{o||=await this.openResidentHandle(e);try{await o.handle.writeFile(i)}catch{await this.invalidateHandle(e),o=await this.openResidentHandle(e),await o.handle.writeFile(i)}}catch(t){throw await this.invalidateHandle(e).catch(()=>void 0),new m(`Failed to append to stream \"${e}\"`,{cause:t})}let s=this.byteIndexes.get(e);return s&&s.tailSeq===n&&this.recordIndexPoint(e,n,o.tailByteOffset,a),o.tailByteOffset+=a,o.lastUsedAt=Date.now(),this.seqCounters.set(e,n+1),r}async openResidentHandle(e){let t=Date.now();for(let[n,r]of this.handles)n!==e&&t-r.lastUsedAt>6e4&&(this.handles.delete(n),await r.handle.close().catch(()=>void 0));for(;this.handles.size>=8;){let e,t=1/0;for(let[n,r]of this.handles)r.lastUsedAt<t&&(t=r.lastUsedAt,e=n);if(e===void 0)break;let n=this.handles.get(e);this.handles.delete(e),await n.handle.close().catch(()=>void 0)}let n=await r(this.resolvePath(e),U),{size:i}=await n.stat(),a={handle:n,lastUsedAt:t,tailByteOffset:i};return this.handles.set(e,a),a}async invalidateHandle(e){let t=this.handles.get(e);t&&(this.handles.delete(e),await t.handle.close().catch(()=>void 0))}recordIndexPoint(e,t,n,r){let i=this.byteIndexes.get(e);!i||t<i.tailSeq||(t%G===0&&q(i,{seq:t,byteOffset:n}),i.tailSeq=t+1,i.tailByteOffset=n+r)}ensureByteIndex(e){let t=this.byteIndexes.get(e);return t||(t={anchors:[],tailSeq:0,tailByteOffset:0},this.byteIndexes.set(e,t)),t}nearestAnchor(e,t){let n={seq:0,byteOffset:0};for(let r of e.anchors)if(r.seq<=t&&r.seq>=n.seq)n=r;else if(r.seq>t)break;return n}async repairPartialTail(e){await this.invalidateHandle(e);let t=this.resolvePath(e),n;try{n=await r(t,`r+`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let{size:e}=await n.stat();if(e===0)return;let t=Buffer.alloc(1);if(await n.read(t,0,1,e-1),t[0]===10)return;let r=-1,i=e;for(;i>0&&r===-1;){let e=Math.max(0,i-65536),t=Buffer.alloc(i-e);await n.read(t,0,t.length,e);for(let n=t.length-1;n>=0;n--)if(t[n]===10){r=e+n+1;break}i=e}await n.truncate(r===-1?0:r)}catch(t){throw new m(`Failed to repair stream \"${e}\"`,{cause:t})}finally{await n.close()}}async*read(e,t){let n=t?.from??0,i=t?.to??1/0;if(i<=n)return;let a;try{a=await r(this.resolvePath(e),`r`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let t=this.ensureByteIndex(e),r=n<t.tailSeq?this.nearestAnchor(t,n):{seq:t.tailSeq,byteOffset:t.tailByteOffset},o=Buffer.alloc(W),s=new p(`utf-8`),c=``,l=r.seq,u=r.byteOffset,d=r.byteOffset;for(;;){let r=d,{bytesRead:f}=await a.read(o,0,W,d);if(f===0)break;d+=f;let p=0;for(;l<n&&l<t.tailSeq&&p<f;){let e=o.subarray(p,f).indexOf(10);if(e===-1){p=f;break}p+=e+1,l++,u=r+p}if(p>=f)continue;c+=s.write(o.subarray(p,f));let m;for(;(m=c.indexOf(`\n`))>=0;){let r=c.slice(0,m),a=u+Buffer.byteLength(r,`utf-8`)+1;if(c=c.slice(m+1),r.length===0){u=a;continue}if(l>=t.tailSeq&&(l%G===0&&q(t,{seq:l,byteOffset:u}),t.tailSeq=l+1,t.tailByteOffset=a),l>=n&&(yield{seq:l,streamId:e,entry:this.codec.decode(r)}),l++,u=a,l>=i)return}}}finally{await a.close()}}async count(e){let t=this.seqCounters.get(e);if(t!==void 0)return t;let n=await this.countLines(e);return this.seqCounters.set(e,n),n}async*tail(e){let t=this.resolvePath(e),n=0,i=0,a=``;for(;!this.closed;){let o;try{o=await r(t,`r`)}catch(t){if(Z(t)){await X(this.pollIntervalMs);continue}throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let{size:t}=await o.stat();if(t<i&&(i=0,n=0,a=``),t>i){let r=t-i,s=Buffer.alloc(r);await o.read(s,0,r,i),i=t,a+=s.toString(`utf-8`);let c;for(;(c=a.indexOf(`\n`))>=0;){let t=a.slice(0,c);a=a.slice(c+1),t.length!==0&&(yield{seq:n,streamId:e,entry:this.codec.decode(t)},n++)}}}finally{await o.close()}if(this.closed)break;await X(this.pollIntervalMs)}}async close(){this.closed=!0,await this.writeQueue.catch(()=>void 0);for(let[,e]of this.handles)await e.handle.close().catch(()=>void 0);this.handles.clear()}truncate(e,t){if(t<=0)return this.clear(e);let n=this.writeQueue.then(()=>this.truncateSerial(e,t));return this.writeQueue=n.catch(()=>void 0),n}async truncateSerial(e,t){await this.invalidateHandle(e);let n;try{n=await r(this.resolvePath(e),`r+`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let e=Buffer.alloc(W),r=0,i=0,a=-1;outer:for(;;){let{bytesRead:o}=await n.read(e,0,W,null);if(o===0)break;for(let n=0;n<o;n++)if(e[n]===10&&(r++,r===t)){a=i+n+1;break outer}i+=o}if(a===-1)return;await n.truncate(a)}catch(t){throw new m(`Failed to truncate stream \"${e}\"`,{cause:t})}finally{await n.close()}this.seqCounters.set(e,t),this.byteIndexes.delete(e)}clear(e){let t=this.writeQueue.then(()=>this.clearSerial(e));return this.writeQueue=t.catch(()=>void 0),t}async clearSerial(e){await this.invalidateHandle(e),this.seqCounters.delete(e),this.byteIndexes.delete(e);try{await c(this.resolvePath(e))}catch(t){if(!Z(t))throw new m(`Failed to clear stream \"${e}\"`,{cause:t})}}async countLines(e){let t;try{t=await r(this.resolvePath(e),`r`)}catch(t){if(Z(t))return 0;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let e=Buffer.alloc(W),n=0;for(;;){let{bytesRead:r}=await t.read(e,0,W,null);if(r===0)break;for(let t=0;t<r;t++)e[t]===10&&n++}return n}finally{await t.close()}}async nextSeq(e){let t=this.seqCounters.get(e);if(t!==void 0)return t;let n=await this.countLines(e);return this.seqCounters.set(e,n),n}resolvePath(e){return t(this.baseDir,`${encodeURIComponent(e)}${this.extension}`)}async ensureDir(){this.dirReady||=(await n(this.baseDir,{recursive:!0}),!0)}};function ce(e){switch(e.type){case`memory`:return new J;case`file`:return new Y({baseDir:e.baseDir,extension:e.extension,codec:e.codec,pollIntervalMs:e.pollIntervalMs});default:throw Error(`Unsupported append-log type: ${e.type}`)}}var le=class{subs=new Map;publish(e){let t=this.subs.get(e.streamId);if(t)for(let n of[...t])n(e)}subscribe(e,t){let n=this.subs.get(e);return n||this.subs.set(e,n=new Set),n.add(t),()=>{let n=this.subs.get(e);n&&(n.delete(t),n.size===0&&this.subs.delete(e))}}};function ue(e){let t=new le;return{store:{async append(n,r){let i=await e.append(n,r);return t.publish(i),i},read:(t,n)=>e.read(t,n),tail:t=>e.tail(t),truncate:(t,n)=>e.truncate(t,n),clear:t=>e.clear(t),...e.count?{count:t=>e.count(t)}:{},...e.close?{close:()=>e.close()}:{}},live:t}}function X(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function Z(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}function Q(e){let t=e;return typeof t.getChangeLog==`function`&&typeof t.getChangesSince==`function`&&typeof t.getDeletedSince==`function`}const de=[`sessions`];var $=class{namespaces;backend;now;instances=new Map;appendLogs=new Map;constructor(e){this.namespaces=new Set(e.namespaces),this.backend=e.backend,this.now=e.now??(()=>Date.now())}has(e){return this.namespaces.has(e)}listNamespaces(){return[...this.namespaces]}wsId(e,t){return`${e?.trim()||`default`}/${t}`}async appendEntry(e,t,n,r){if(!this.namespaces.has(e))throw Error(`Unknown storage namespace: ${e}`);let i=this.appendLogs.get(e);return i||(i=new J,this.appendLogs.set(e,i)),i.append(this.wsId(t,n),r)}async save(e,t,n,r,i){await S(this.resolve(e),this.wsId(t,n),r,{now:this.now(),owner:i,updatedBy:i})}async updateMetadata(e,t,n,r,i){let a=this.resolve(e),o=await a.load(this.wsId(t,n));if(!o)throw Error(`Document \"${n}\" not found in \"${e}\"`);o.metadata={...o.metadata,...r},i&&(o.metadata.updatedBy=i),await a.save(o)}async load(e,t,n){let r=await this.resolve(e).load(this.wsId(t,n));return r?r.data:null}async delete(e,t,n){return this.resolve(e).delete(this.wsId(t,n))}async list(e,t,n,r){let i=await this.resolve(e).list(),a=`${t||`default`}/`,o=[];for(let e of i)e.startsWith(a)&&o.push({clean:e.slice(a.length),prefixed:e});let s=o.map(e=>e.clean);if(!n||!r||r===`all`)return s;let c=[];for(let{clean:t,prefixed:i}of o){let a=await this.resolve(e).load(i);if(!a)continue;let o=a.metadata;(r===`owned`&&o.owner===n||r===`visible`&&(o.owner===n||o.visibility===`shared`||!o.visibility))&&c.push(t)}return c}async getChangesSince(e,t,n,r){let i=this.resolve(e),a=t?`${t}/`:`default/`;if(Q(i)){let t=r?.map(e=>`${a}${e}`),o=i.getChangesSince(e,n,t),s=new Map;for(let e of o.ids){if(!e.startsWith(a))continue;let t=e.slice(a.length),n=await i.load(e);s.set(t,n?.data??null)}let c=i.getDeletedSince(e,n);for(let e of c){if(!e.startsWith(a))continue;let t=e.slice(a.length);s.has(t)||s.set(t,null)}return{documents:s,cursor:o.cursor}}let o=await i.list(),s=new Map,c=n;for(let e of o){if(!e.startsWith(a))continue;let t=e.slice(a.length);if(r&&!r.includes(t))continue;let o=await i.load(e);o&&o.metadata.updatedAt>n&&(s.set(t,o.data),c=Math.max(c,o.metadata.updatedAt))}return{documents:s,cursor:c}}async listPaginated(e,t,n){let r=this.resolve(e),i=await r.list(),a=`${t||`default`}/`,o=new Map;for(let e of i)e.startsWith(a)&&o.set(e,e.slice(a.length));let s=[];for(let[e,t]of o){let n=await r.load(e);n&&s.push({...n,id:t})}let c=n.order===`asc`?1:-1;return s.sort((e,t)=>c*(this.sortValue(e,n.sortBy)-this.sortValue(t,n.sortBy))),y(s.map(e=>e.id),{page:n.page,pageSize:n.pageSize})}sortValue(e,t){let n=e.metadata[t];if(typeof n==`number`)return n;let r=e.data?.metadata?.[t];return typeof r==`number`?r:e.metadata.updatedAt}async*streamEvents(e,t,n,r=500,i){if(!this.namespaces.has(e))return;let a=this.resolve(e);if(!Q(a))return;let o=n,s=-1;for(;!i?.aborted&&(await pe(r,i),!i?.aborted);){let n=a.getChangeLog(e,o),r=`${t||`default`}/`;for(let e of n){if(i?.aborted)break;e.id<=s||e.doc_id.startsWith(r)&&(yield{...e,doc_id:e.doc_id.slice(r.length)},o=Math.max(o,e.ts),s=Math.max(s,e.id))}}}resolve(e){if(!this.namespaces.has(e))throw Error(`Unknown storage namespace: ${e}`);let t=this.instances.get(e);return t||(t=this.backend(e),this.instances.set(e,t)),t}};function fe(e){return new $({namespaces:e.namespaces??de,backend:n=>new j({baseDir:t(e.baseDir,n)}),now:e.now})}function pe(e,t){return new Promise(n=>{if(t?.aborted){n();return}let r=setTimeout(n,e);t&&t.addEventListener(`abort`,()=>{clearTimeout(r),n()},{once:!0})})}var me=class{baseUrl;getAuth;getHeaders;fetchImpl;timeoutMs;pollIntervalMs;flushAtSize;codec;buffer=new Map;flushingStreams=new Set;flushTimer;closed=!1;constructor(e){this.baseUrl=e.baseUrl.replace(/\\/+$/,``),this.getAuth=e.getAuth,this.getHeaders=e.getHeaders??(async()=>({})),this.fetchImpl=e.fetch??globalThis.fetch,this.timeoutMs=e.timeoutMs??3e4,this.pollIntervalMs=e.pollIntervalMs??500,this.flushAtSize=e.flushAtSize??100,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e)};let t=e.flushIntervalMs??2e3;this.flushTimer=setInterval(()=>{this.flushAll().catch(()=>{})},t),this.flushTimer.unref?.()}async append(e,t){let n=this.codec.encode(t);return new Promise((t,r)=>{if(this.closed){r(Error(`HttpAppendLog closed — buffered events lost`));return}let i=this.buffer.get(e)??[];i.push({encoded:n,resolve:t,reject:r}),this.buffer.set(e,i),i.length>=this.flushAtSize&&this.tryFlush(e).catch(()=>{})})}async tryFlush(e){if(this.flushingStreams.has(e))return;let t=this.buffer.get(e);if(!(!t||t.length===0)){this.flushingStreams.add(e);try{await this.flushOne(e,t)}finally{this.flushingStreams.delete(e)}}}async flushOne(e,t){this.buffer.delete(e);try{let{seqs:n}=await this.request(`/trace/${encodeURIComponent(e)}`,`POST`,t.map(e=>e.encoded));for(let r=0;r<t.length;r++){let i=n[r]??-1,a=this.codec.decode(t[r].encoded);t[r].resolve({seq:i,streamId:e,entry:a})}}catch{if(this.closed)for(let e of t)e.reject(Error(`HttpAppendLog closed — buffered events lost`));else{let n=this.buffer.get(e)??[];this.buffer.set(e,[...n,...t])}}}async flushAll(){let e=[...this.buffer.keys()];for(let t of e){if(this.flushingStreams.has(t))continue;let e=this.buffer.get(t);if(e&&e.length>0){this.flushingStreams.add(t);try{await this.flushOne(t,e)}finally{this.flushingStreams.delete(t)}}}}async*read(e,t){let n=new URLSearchParams;t?.from!==void 0&&n.set(`from`,String(t.from)),t?.to!==void 0&&n.set(`to`,String(t.to));let r=n.toString(),i=`/trace/${encodeURIComponent(e)}${r?`?${r}`:``}`,{events:a}=await this.request(i,`GET`);for(let n=0;n<a.length;n++){let r=this.codec.decode(a[n]);yield{seq:(t?.from??0)+n,streamId:e,entry:r}}}async*tail(e){let t=-1;{let n=`/trace/${encodeURIComponent(e)}`,{events:r}=await this.request(n,`GET`);for(let n=0;n<r.length;n++){let i=this.codec.decode(r[n]),a=n;t=a,yield{seq:a,streamId:e,entry:i}}}for(;!this.closed;){let n=t+1,r=`/trace/${encodeURIComponent(e)}?from=${n}`;try{let{events:i}=await this.request(r,`GET`);for(let r=0;r<i.length;r++){let a=this.codec.decode(i[r]),o=n+r;t=o,yield{seq:o,streamId:e,entry:a}}}catch{}await he(this.pollIntervalMs)}}async count(e){let{count:t}=await this.request(`/trace/${encodeURIComponent(e)}/count`,`GET`);return t+(this.buffer.get(e)?.length??0)}async truncate(e,t){await this.request(`/trace/${encodeURIComponent(e)}/truncate`,`POST`,{toSeq:t})}async clear(e){await this.request(`/trace/${encodeURIComponent(e)}`,`DELETE`)}async close(){this.closed=!0,this.flushTimer&&=(clearInterval(this.flushTimer),void 0),await this.flushAll();let e=Date.now()+this.timeoutMs+5e3;for(;this.flushingStreams.size>0&&Date.now()<e;)await new Promise(e=>setTimeout(e,50));await this.flushAll()}async request(e,t,n){let r=await this.getAuth(),i={...await this.getHeaders(),...r.token?{Authorization:`Bearer ${r.token}`}:{},Accept:`application/json`};n!==void 0&&(i[`Content-Type`]=`application/json`);let a=new AbortController,o=setTimeout(()=>a.abort(),this.timeoutMs),s=`${this.baseUrl}${e}`;try{let e=await this.fetchImpl(s,{method:t,headers:i,body:n===void 0?void 0:JSON.stringify(n),signal:a.signal});if(!e.ok)throw Error(`HTTP ${e.status}: ${e.statusText}`);return await e.json()}finally{clearTimeout(o)}}};function he(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}export{N as Database,N as default,te as DiskPanelStatePersistence,re as DiskPasteStatePersistence,C as DiskPersistence,Y as FileAppendLog,j as FilePersistence,me as HttpAppendLog,J as MemoryAppendLog,x as MemoryPersistence,b as MemoryPersistenceBase,m as PersistenceError,ie as RemotePersistence,h as RemotePersistenceError,g as RemoteSnapshotConflictError,V as SqlitePersistence,$ as StorageHost,z as acquireDb,_ as atomicWriteFile,ce as createAppendLog,fe as createFileStorageHost,ne as createInMemoryPanelStatePersistence,A as createInMemoryPasteStatePersistence,ue as createLiveTappedAppendLog,H as createPersistence,k as normalizePasteSnapshot,v as pageBounds,y as paginate,B as releaseDb,O as resolvePasteNextId,S as saveDocument};\n//# sourceMappingURL=index.js.map","import { Database } from '@x-otto/persistence'\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 // RFC-354 P3-1:`loadRow` 按 (id, userId) 过滤,所以\"读不到\"有两种含义——\n // ①该 id 确实不存在;②该 id 存在但**属于另一个用户**。此前两种情况都直接\n // 返回 `{ok:true, revision:1}`,而 `INSERT OR IGNORE` 在情况②会撞主键静默\n // no-op → 路由回 200 + ETag,但数据从未落盘、该用户也读不回来(实证:A 的\n // 行纹丝不动,B 写入彻底丢失却收到成功回执)。\n //\n // 用 `changes` 区分:0 = 主键已被他人占用,属归属冲突而非并发修改,回 false\n // 让路由走 409。不返回 403 是因为本层不应泄露\"该 id 被谁占用\"的存在性信息,\n // 且路由层已有 409 通道(`currentRevision` 对该用户同样返回 undefined,\n // 客户端据此可知不是简单的 revision 落后)。\n const result = stmts.insert.run(id, data, userId ?? null, now, now)\n if (result.changes === 0) {\n return { ok: false as const }\n }\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, session_id, seq)——不同用户的同名 session 互不可见。\n // SQLite 不支持 ALTER TABLE 改主键——旧表需重建。\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 // 旧表无 user_id 列——重建表结构(数据迁移:旧数据 user_id='' 保留,历史 trace 仍可被\n // userId='' 的请求访问——多租户启用前的数据不会丢失,只是不属于任何新租户)。\n db.exec('ALTER TABLE trace_events RENAME TO trace_events_old')\n db.exec(`\n CREATE TABLE 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 db.exec(`\n INSERT INTO trace_events (user_id, session_id, seq, data, ts)\n SELECT '', session_id, seq, data, ts FROM trace_events_old\n `)\n db.exec('DROP TABLE trace_events_old')\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 const appendTxn = db.transaction((userId: string, sessionId: string, data: string, now: number): number => {\n // RFC-353 S11: INSERT 的子查询 MAX(seq)+1 与随后单独的 SELECT MAX(seq) 之间存在\n // 理论竞态窗口——若本 store 未来被多连接/多进程共享(当前是单进程单连接,better-sqlite3\n // 同步 API 天然串行化,无实际竞态),INSERT 提交后另一连接可能在本连接读 lastSeq 前\n // 插入更高的 seq,导致 append() 返回值不是\"本次 append 真正分配到的 seq\"。\n // 显式事务包裹 INSERT+SELECT,使两者对其他连接呈现为一个原子操作,消除该窗口,\n // 不依赖\"当前部署形态恰好是单连接\"这一隐含假设。\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 return {\n append(userId: string, sessionId: string, data: string): number {\n const now = Date.now()\n return appendTxn(userId, sessionId, data, now)\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 { paginate } from '@x-otto/persistence'\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 // RFC-372 S8:此前路由恒返回 `{ ids }`,但客户端 `RemotePersistence.listPaginated()`\n // 期望 `PaginatedResult<string>` 形状(items/total/page/pageSize/totalPages/hasNext/\n // hasPrevious)——两者完全不匹配,是既存的契约缺口。现在返回完整 PaginatedResult\n // 形状,使客户端能正确解析分页语义。query 参数此前被完全忽略,现在消费 page/\n // pageSize 做内存分页(ids 全量返回后切片,数据量级可控)。\n //\n // F12:0-based page 契约对齐(client 原样发送 0=第一页)。用显式 Number.isFinite\n // 判断而非 `|| 0`——`parseInt('0')||1` 会把合法首页 0 误判为缺省值升成 1,导致\n // page=0 与 page=1 都被压成 1、第二页永远拿第一页(本次一并修复)。\n const parsedPage = Number.parseInt(c.req.query('page') ?? '', 10)\n const parsedPageSize = Number.parseInt(c.req.query('pageSize') ?? '', 10)\n const page = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0\n const pageSize = Number.isFinite(parsedPageSize) && parsedPageSize > 0 ? parsedPageSize : 50\n\n // sortBy/order:`store.list()` 返回的 ids 是 string[],本身无 per-id 排序键;其顺序由\n // 存储层固定为 updated_at DESC。因此 order=desc 保持该顺序、order=asc 反转为\n // updated_at ASC。sortBy 无法选择 createdAt 等其它键(存储层不暴露),接受后忽略并\n // 回退 updated_at 语义——记录为语义缺口,而非静默错序。\n const order = c.req.query('order') === 'asc' ? 'asc' : 'desc'\n const ordered = order === 'asc' ? [...ids].reverse() : ids\n\n return c.json(paginate(ordered, { page, pageSize }))\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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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["f","u","Database","paginate"],"sources":["../src/config.ts","../../persistence/dist/index.js","../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{dirname as e,join as t}from\"node:path\";import{mkdir as n,open as r,readFile as i,readdir as a,rename as o,stat as s,unlink as c}from\"node:fs/promises\";import{randomUUID as l}from\"node:crypto\";import{constants as u,mkdirSync as d}from\"node:fs\";import{DatabaseSync as f}from\"node:sqlite\";import{StringDecoder as p}from\"node:string_decoder\";var m=class extends Error{constructor(e,t){super(e,t),this.name=`PersistenceError`}},h=class extends m{statusCode;constructor(e,t,n){super(e,n),this.name=`RemotePersistenceError`,this.statusCode=t}},g=class extends h{info;constructor(e,t,n){super(e,409,n),this.name=`RemoteSnapshotConflictError`,this.info=t}};async function _(t,n){let i=`${t}.${l()}.tmp`;try{let a=await r(i,`w`);try{await a.writeFile(n),await a.sync()}finally{await a.close()}await o(i,t),await ee(e(t))}catch(e){throw await c(i).catch(()=>{}),e instanceof m?e:new m(`Failed to atomically write \"${t}\"`,{cause:e})}}async function ee(e){let t;try{t=await r(e,`r`),await t.sync()}catch{}finally{t&&await t.close().catch(()=>{})}}function v(e,t){let n=t.pageSize>=1?t.pageSize:1,r=Math.max(1,Math.ceil(e/n)),i=Math.max(0,Math.min(t.page,r-1));return{total:e,page:i,pageSize:n,totalPages:r,start:i*n,hasNext:i<r-1,hasPrevious:i>0}}function y(e,t){let n=v(e.length,t);return{items:e.slice(n.start,n.start+n.pageSize),total:n.total,page:n.page,pageSize:n.pageSize,totalPages:n.totalPages,hasNext:n.hasNext,hasPrevious:n.hasPrevious}}var b=class{store=new Map;defaultPageSize;defaultSortBy;constructor(e={}){this.defaultPageSize=e.defaultPageSize??20,this.defaultSortBy=e.defaultSortBy??`updatedAt`}getSortValue(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}async save(e){this.store.set(this.extractId(e),structuredClone(e))}async load(e){let t=this.store.get(e);return t?structuredClone(t):null}async delete(e){return this.store.delete(e)}async list(){return Array.from(this.store.keys())}entries(){return Array.from(this.store.entries())}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??this.defaultSortBy,i=e.pageSize??this.defaultPageSize,a=Array.from(this.store.entries()).map(([e,t])=>({id:e,sortValue:this.getSortValue(t,r)}));return a.sort((e,t)=>n===`asc`?e.sortValue-t.sortValue:t.sortValue-e.sortValue),y(a.map(e=>e.id),{page:t,pageSize:i})}},x=class extends b{extractId(e){return e.id}};async function S(e,t,n,r){let{now:i,version:a=1,tags:o=[],preserveCreatedAt:s=!0,owner:c,updatedBy:l}=r,u=s?await e.load(t):null,d=u?.metadata?.owner;await e.save({version:a,id:t,data:n,metadata:{createdAt:u?.metadata.createdAt??i,updatedAt:i,tags:o,...d?{owner:d}:c?{owner:c}:{},...l?{updatedBy:l}:{}}})}var C=class{baseDir;extension;defaultPageSize;codec;initialized=!1;constructor(e){this.baseDir=e.baseDir,this.extension=e.extension??`.json`,this.defaultPageSize=e.defaultPageSize??20,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`}}async save(e){await this.ensureDir(),await _(this.resolvePath(this.extractId(e)),this.codec.encode(e))}async load(e){await this.ensureDir();let t=this.resolvePath(e);try{let e=await i(t,`utf-8`);return this.codec.decode(e)}catch(t){if(w(t))return null;throw new m(`Failed to load \"${e}\"`,{cause:t})}}async delete(e){try{return await c(this.resolvePath(e)),!0}catch{return!1}}async list(){await this.ensureDir();try{return(await a(this.baseDir)).filter(e=>e.endsWith(this.extension)).map(e=>decodeURIComponent(e.slice(0,-this.extension.length)))}catch{return[]}}async listPaginated(e){await this.ensureDir();let{page:n,order:r=`desc`}=e,i=e.pageSize??this.defaultPageSize;try{let e=(await a(this.baseDir)).filter(e=>e.endsWith(this.extension)),o=await Promise.all(e.map(async e=>{let n=await s(t(this.baseDir,e));return{id:decodeURIComponent(e.slice(0,-this.extension.length)),mtime:n.mtimeMs}}));return o.sort((e,t)=>r===`asc`?e.mtime-t.mtime:t.mtime-e.mtime),y(o.map(e=>e.id),{page:n,pageSize:i})}catch{return{items:[],total:0,page:0,pageSize:i,totalPages:1,hasNext:!1,hasPrevious:!1}}}resolvePath(e){let n=encodeURIComponent(e);return t(this.baseDir,`${n}${this.extension}`)}async ensureDir(){this.initialized||=(await n(this.baseDir,{recursive:!0}),!0)}};function w(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}var T=class{map=new Map;constructor(e){this.extractId=e}async save(e){this.map.set(this.extractId(e),e)}async load(e){return this.map.get(e)??null}async delete(e){return this.map.delete(e)}},E=class extends C{extractId(e){return e.sessionId}},te=class{store;constructor(e){this.store=new E({baseDir:e.baseDir,extension:`.json`})}async save(e){return this.store.save(e)}async load(e){return this.store.load(e)}async delete(e){return this.store.delete(e)}};function ne(){return new T(e=>e.sessionId)}var D=class extends C{extractId(e){return e.sessionId}},re=class{store;constructor(e){this.store=new D({baseDir:e.baseDir,extension:`.json`})}async save(e){return this.store.save(e)}async load(e){return this.store.load(e)}async delete(e){return this.store.delete(e)}};function O(e){let t=typeof e==`object`&&e?e:{},n=typeof t.nextId==`number`&&Number.isFinite(t.nextId)?Math.floor(t.nextId):0,r=Math.max(1,n),i=Array.isArray(t.entries)?t.entries:[];for(let e of i){let t=e?.id;typeof t==`number`&&Number.isFinite(t)&&(r=Math.max(r,Math.floor(t)+1))}return r}function k(e,t){let n=typeof t==`object`&&t?t:{},r=Array.isArray(n.entries)?n.entries:[],i=[],a=new Set;for(let e of r){if(typeof e!=`object`||!e)continue;let{id:t,text:n}=e;if(typeof t!=`number`||!Number.isFinite(t)||typeof n!=`string`)continue;let r=Math.floor(t);a.has(r)||(a.add(r),i.push({id:r,text:n,byteLength:Buffer.byteLength(n,`utf-8`)}))}return{sessionId:e,entries:i,nextId:O(n)}}function A(){return new T(e=>e.sessionId)}var ie=class{baseUrl;resolveWsKey;get wsKey(){return this.resolveWsKey()}getAuth;getHeaders;fetch;timeoutMs;defaultPageSize;defaultSortBy;codec;get serviceLabel(){return`远端持久化服务`}get endpointFlag(){return`--persistence-url`}constructor(e){this.baseUrl=e.baseUrl.replace(/\\/+$/,``);let t=e.wsKey;this.resolveWsKey=typeof t==`function`?()=>t()||`default`:()=>t||`default`,this.getAuth=e.getAuth,this.getHeaders=e.getHeaders??(async()=>({})),this.fetch=e.fetch,this.timeoutMs=e.timeoutMs,this.defaultPageSize=e.defaultPageSize??20,this.defaultSortBy=e.defaultSortBy??`updatedAt`,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`}}wsPath(e){return`/${encodeURIComponent(this.wsKey)}/${encodeURIComponent(e)}`}wsListPath(){return`/${encodeURIComponent(this.wsKey)}`}async save(e){await this.request(this.wsPath(this.extractId(e)),{method:`PUT`,body:this.codec.encode(e)})}async saveWithRevision(e,t){let n={};t!==void 0&&(n[`If-Match`]=String(t));let r=(await this.request(this.wsPath(this.extractId(e)),{method:`PUT`,body:this.codec.encode(e)},n)).headers.get(`ETag`);return r?Number(r.replace(/\"/g,``)):NaN}async loadWithRevision(e){let t=await this.request(this.wsPath(e),{method:`GET`});if(t.status===404)return null;let n=await t.text(),r;try{r=this.codec.decode(n)}catch(n){throw new h(`Remote persistence decode failed: ${this.wsPath(e)}`,t.status,{cause:n})}let i=t.headers.get(`ETag`),a=i?Number(i.replace(/\"/g,``)):0;return{snapshot:r,revision:a}}async load(e){return(await this.loadWithRevision(e))?.snapshot??null}async delete(e){return(await this.request(this.wsPath(e),{method:`DELETE`})).ok}async list(){let e=await(await this.request(this.wsListPath(),{method:`GET`})).json();return e.items??e.ids??[]}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??this.defaultSortBy,i=e.pageSize??this.defaultPageSize,a=new URLSearchParams({page:String(t),pageSize:String(i),sortBy:r,order:n});return(await this.request(`${this.wsListPath()}?${a.toString()}`,{method:`GET`})).json()}async request(e,t,n){let r=await this.getAuth(),i={...await this.getHeaders(),...r.token?{Authorization:`Bearer ${r.token}`}:{},Accept:`application/json`,...n};t.body&&(i[`Content-Type`]=this.codec.contentType??`application/json`);let a=new AbortController,o=setTimeout(()=>a.abort(),this.timeoutMs),s=`${this.baseUrl}${e}`;try{let n=await this.fetch(s,{method:t.method,headers:i,body:t.body,signal:a.signal});if(n.status===409){let t=null;try{let e=await n.clone().json();t=typeof e.revision==`number`?e.revision:null}catch{}throw new g(`Remote persistence CAS conflict: ${this.wsPath(e.split(`/`).pop()??``)}`,{revision:t})}if(!n.ok&&n.status!==404)throw new h(`Remote persistence API error: ${n.status} ${n.statusText}`,n.status);return n}catch(e){if(e instanceof g||e instanceof h)throw e;if(e instanceof DOMException&&e.name===`AbortError`)throw new h(`无法连接到${this.serviceLabel}(请求超时 ${this.timeoutMs/1e3}s):${t.method} ${s}\\n提示:请确认${this.serviceLabel}是否已启动、${this.endpointFlag} 地址与端口是否正确。`,0,{cause:e});let n=e?.cause?.code,r=n?n===`ECONNREFUSED`?`(连接被拒绝)`:n===`ENOTFOUND`?`(DNS 解析失败,地址不存在)`:n===`ECONNRESET`?`(连接被重置)`:n===`ETIMEDOUT`?`(连接超时)`:n===`CERT_HAS_EXPIRED`?`(TLS 证书已过期)`:n===`DEPTH_ZERO_SELF_SIGNED_CERT`?`(自签名证书被拒)`:`(${n})`:``,i=e instanceof Error?e.message:String(e);throw new h(`无法连接到${this.serviceLabel}:${t.method} ${s}\\n原因:${i}${r}\\n提示:请确认${this.serviceLabel}是否已启动、${this.endpointFlag} 地址与端口是否正确。`,0,{cause:e})}finally{clearTimeout(o)}}},j=class extends C{constructor(e){super({baseDir:e.baseDir,extension:e.extension,codec:e.codec})}extractId(e){return e.id}};const ae=process.emitWarning;process.emitWarning=function(e,...t){if(!(typeof e==`string`&&e.includes(`SQLite is an experimental feature`)))return ae.call(process,e,...t)};function oe(e){let t=e=>e.map(e=>e===void 0?null:e);return{run(...n){return e.run(...t(n))},get(...n){return e.get(...t(n))},all(...n){return e.all(...t(n))},setReadBigInts(t){e.setReadBigInts(t)}}}let M=0;var N=class{db;name;constructor(e,t){this.db=t?new f(e,t):new f(e),this.name=e}prepare(e){return oe(this.db.prepare(e))}exec(e){this.db.exec(e)}close(){this.db.close()}pragma(e,t){if(e.includes(`=`)){this.db.exec(`PRAGMA ${e}`);return}let n=this.db.prepare(`PRAGMA ${e}`).all();if(t?.simple){if(n.length===0)return;let e=n[0];return Object.values(e)[0]}return n}transaction(e){let t=t=>(...n)=>{let r=`sp_${M}`;M++===0?this.db.exec(t?`BEGIN IMMEDIATE`:`BEGIN`):this.db.exec(`SAVEPOINT ${r}`);try{let t=e(...n);return--M===0?this.db.exec(`COMMIT`):this.db.exec(`RELEASE ${r}`),t}catch(e){throw--M===0?this.db.exec(`ROLLBACK`):(this.db.exec(`ROLLBACK TO ${r}`),this.db.exec(`RELEASE ${r}`)),e}},n=t(!1);return n.immediate=t(!0),n}};const P=new Map,F=[{version:1,up:e=>{e.exec(`\n -- snapshots (SqlitePersistence)\n CREATE TABLE IF NOT EXISTS snapshots (\n ns TEXT NOT NULL,\n id TEXT NOT NULL,\n version INTEGER NOT NULL DEFAULT 1,\n data TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n tags TEXT NOT NULL DEFAULT '[]',\n PRIMARY KEY (ns, id)\n );\n CREATE INDEX IF NOT EXISTS idx_snapshots_ns_updated\n ON snapshots(ns, updated_at DESC);\n CREATE INDEX IF NOT EXISTS idx_snapshots_ns_created\n ON snapshots(ns, created_at DESC);\n\n -- change_log (SqlitePersistence)\n CREATE TABLE IF NOT EXISTS change_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n op TEXT NOT NULL,\n ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_change_log_ns_ts\n ON change_log(ns, ts);\n `)}},{version:2,up:e=>{e.exec(`\n -- append_log(RFC-089 M28 事件溯源日志表结构;消费实现 SqliteAppendLog 已删除——\n -- 终局架构 review 2026-07-10 P1-7 确认零生产接线,迁移本身保留不动以维持已发布\n -- migration 历史的不可变性,仅此注释更新去掉对已删除类名的引用)\n CREATE TABLE IF NOT EXISTS append_log (\n stream_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n payload TEXT NOT NULL,\n PRIMARY KEY (stream_id, seq)\n );\n `)}}],I=new Set;function L(e){return e.reduce((e,t)=>Math.max(e,t.version),0)}function R(e,t,n){let r=L(n);if(t===`:memory:`){if(e.pragma(`user_version`,{simple:!0})===0){for(let t of n)t.up(e);e.pragma(`user_version = ${r}`)}return}if(I.has(t))return;let i=e.pragma(`user_version`,{simple:!0});if(i===0)for(let t of n)t.up(e);else if(i<r)for(let t of n)t.version>i&&t.up(e);else if(i>r)throw Error(`DB at ${t} has schema v${i} > code v${r}. Downgrade not supported. Upgrade otto first.`);e.pragma(`user_version = ${r}`),I.add(t)}function z(t,n=!0,r=F){if(t===`:memory:`){let e=new N(`:memory:`);return R(e,t,r),e}let i=P.get(t);if(i){let e=L(r);if(i.migrationTarget!==e)throw Error(`acquireDb cache conflict at ${t}: cached schema v${i.migrationTarget} but requested migrationSet targets v${e}. 同一 db 路径不可注入不同 migration 段。`);return i.refs++,i.db}d(e(t),{recursive:!0});let a=new N(t);return n&&(a.pragma(`journal_mode = WAL`),a.pragma(`synchronous = NORMAL`),a.pragma(`busy_timeout = 5000`)),R(a,t,r),P.set(t,{db:a,refs:1,migrationTarget:L(r)}),a}function B(e){if(e.name===`:memory:`){e.close();return}let t=e.name,n=P.get(t);n&&(n.refs--,n.refs<=0&&(e.close(),P.delete(t)))}var V=class{db;ns;defaultPageSize;codec;constructor(e){this.ns=e.namespace,this.defaultPageSize=e.defaultPageSize??20,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e),contentType:`application/json`},this.db=z(e.dbPath,e.wal!==!1,F)}close(){B(this.db)}async save(e){let t=e.id,n=e.version??1,r=this.codec.encode(e),i=e.metadata?.updatedAt??Date.now(),a=e.metadata?.createdAt??i,o=JSON.stringify(e.metadata?.tags??[]),s=this.db.prepare(`\n INSERT INTO snapshots (ns, id, version, data, created_at, updated_at, tags)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (ns, id) DO UPDATE SET\n version = excluded.version,\n data = excluded.data,\n updated_at = excluded.updated_at,\n tags = excluded.tags\n `),c=this.db.prepare(`INSERT INTO change_log (ns, doc_id, op, ts) VALUES (?, ?, 'save', ?)`);this.db.transaction(()=>{s.run(this.ns,t,n,r,a,i,o),c.run(this.ns,t,i)})()}async load(e){let t=this.db.prepare(`SELECT data FROM snapshots WHERE ns = ? AND id = ?`).get(this.ns,e);return t?this.codec.decode(t.data):null}async delete(e){let t=this.db.prepare(`DELETE FROM snapshots WHERE ns = ? AND id = ?`),n=this.db.prepare(`INSERT INTO change_log (ns, doc_id, op, ts) VALUES (?, ?, 'delete', ?)`);return this.db.transaction(()=>t.run(this.ns,e).changes>0?(n.run(this.ns,e,Date.now()),!0):!1)()}async list(){return this.db.prepare(`SELECT id FROM snapshots WHERE ns = ? ORDER BY updated_at DESC`).all(this.ns).map(e=>e.id)}async listPaginated(e){let{page:t,order:n=`desc`}=e,r=e.sortBy??`updated_at`,i=e.pageSize??this.defaultPageSize,a=r===`created_at`?`created_at`:`updated_at`,o=n===`asc`?`ASC`:`DESC`,{count:s}=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots WHERE ns = ?`).get(this.ns),c=v(s,{page:t,pageSize:i}),l=c.start;return{items:this.db.prepare(`SELECT id FROM snapshots WHERE ns = ? ORDER BY ${a} ${o} LIMIT ? OFFSET ?`).all(this.ns,c.pageSize,l).map(e=>e.id),total:c.total,page:c.page,pageSize:c.pageSize,totalPages:c.totalPages,hasNext:c.hasNext,hasPrevious:c.hasPrevious}}getChangeLog(e,t,n=100){return this.db.prepare(`SELECT id, ns, doc_id, op, ts FROM change_log WHERE ns = ? AND ts >= ? ORDER BY ts ASC, id ASC LIMIT ?`).all(e,t,n)}getChangesSince(e,t,n){let r=`SELECT id, updated_at FROM snapshots WHERE ns = ? AND updated_at > ?`,i=[e,t];n&&n.length>0&&(r+=` AND id IN (${n.map(()=>`?`).join(`,`)})`,i.push(...n)),r+=` ORDER BY updated_at ASC`;let a=this.db.prepare(r).all(...i),o=a.length>0?a.reduce((e,t)=>Math.max(e,t.updated_at),0):this.getLatestCursor(e);return{ids:a.map(e=>e.id),cursor:o}}getDeletedSince(e,t){return this.db.prepare(`SELECT doc_id FROM change_log WHERE ns = ? AND op = 'delete' AND ts > ?`).all(e,t).map(e=>e.doc_id)}getLatestCursor(e){return this.db.prepare(`SELECT MAX(updated_at) as ts FROM snapshots WHERE ns = ?`).get(e)?.ts??0}getLatestChangeTs(e){return this.db.prepare(`SELECT MAX(ts) as ts FROM change_log WHERE ns = ?`).get(e)?.ts??0}getInfo(){let e=this.db.pragma(`journal_mode`),[{count:t}]=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots`).all(),[{count:n}]=this.db.prepare(`SELECT COUNT(*) as count FROM change_log`).all(),[{count:r}]=this.db.prepare(`SELECT COUNT(*) as count FROM snapshots WHERE ns = 'sessions'`).all(),i=this.db.pragma(`integrity_check`),a=i.length===1&&i[0]?.integrity_check===`ok`;return{walEnabled:e===`wal`,snapshots:t,changeLogEntries:n,sessions:r,integrity:a?`ok`:`fail: ${JSON.stringify(i)}`}}};function H(e){switch(e.type){case`memory`:return new x;case`file`:return new j({baseDir:e.baseDir,extension:e.extension,codec:e.codec});case`sqlite`:return new V({dbPath:e.dbPath,wal:e.wal,namespace:e.namespace,defaultPageSize:e.defaultPageSize,codec:e.codec});default:throw Error(`Unsupported storage type: ${e.type}`)}}const U=u.O_DSYNC==null?u.O_WRONLY|u.O_CREAT|u.O_APPEND:u.O_WRONLY|u.O_CREAT|u.O_APPEND|u.O_DSYNC,W=1024*1024,G=1e3,K=1e4;function q(e,t){e.anchors.push(t),e.anchors.length>K&&e.anchors.splice(0,Math.ceil(K*.1))}function se(e,t){let n=t?.from??0,r=t?.to??1/0;return e>=n&&e<r}var J=class{streams=new Map;listeners=new Map;maxRecordsPerStream;onEvict;constructor(e){this.maxRecordsPerStream=e?.maxRecordsPerStream??1e4,this.onEvict=e?.onEvict}getOrCreateStream(e){let t=this.streams.get(e);return t||(t={records:[],nextSeq:0,droppedCount:0},this.streams.set(e,t)),t}async append(e,t){let n=this.getOrCreateStream(e),r={seq:n.nextSeq,streamId:e,entry:structuredClone(t)};if(n.nextSeq++,n.records.push(r),n.records.length>this.maxRecordsPerStream){let t=n.records.shift();n.droppedCount++,this.onEvict?.(e,t.seq,n.droppedCount)}let i=this.listeners.get(e);if(i)for(let e of i)e(structuredClone(r));return structuredClone(r)}async*read(e,t){let n=this.streams.get(e)?.records??[];for(let e of n)se(e.seq,t)&&(yield structuredClone(e))}async count(e){return this.streams.get(e)?.nextSeq??0}async*tail(e){let t=[],n=null,r=e=>{t.push(e),n&&=(n(),null)},i=this.listeners.get(e)??new Set;i.add(r),this.listeners.set(e,i);let a=-1;try{for(let t of this.streams.get(e)?.records??[])yield structuredClone(t),a=t.seq;for(;;){t.length===0&&await new Promise(e=>{n=e});for(let e of t.splice(0,t.length))e.seq>a&&(yield e,a=e.seq)}}finally{i.delete(r)}}async truncate(e,t){let n=this.streams.get(e);if(!n)return;if(t<=0){this.streams.delete(e);return}let r=n.records.findIndex(e=>e.seq>=t);r!==-1&&(n.records.length=r,n.nextSeq=t)}async clear(e){this.streams.delete(e)}},Y=class{baseDir;extension;codec;pollIntervalMs;seqCounters=new Map;byteIndexes=new Map;handles=new Map;repairedStreams=new Set;writeQueue=Promise.resolve();dirReady=!1;closed=!1;writerLock;constructor(e){this.baseDir=e.baseDir,this.extension=e.extension??`.jsonl`,this.pollIntervalMs=e.pollIntervalMs??50,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e)},this.writerLock=e.writerLock}append(e,t){if(this.closed)return Promise.resolve({seq:-1,streamId:e,entry:t});let n=this.writeQueue.then(async()=>{if(this.writerLock){await this.writerLock.acquire(e);try{return await this.appendSerial(e,t)}finally{await this.writerLock.release(e).catch(()=>void 0)}}return this.appendSerial(e,t)});return this.writeQueue=n.catch(()=>void 0),n}async appendSerial(e,t){await this.ensureDir(),this.repairedStreams.has(e)||(await this.repairPartialTail(e),this.repairedStreams.add(e));let n=await this.nextSeq(e),r={seq:n,streamId:e,entry:t},i=`${this.codec.encode(t)}\\n`,a=Buffer.byteLength(i,`utf-8`),o=this.handles.get(e);try{o||=await this.openResidentHandle(e);try{await o.handle.writeFile(i)}catch{await this.invalidateHandle(e),o=await this.openResidentHandle(e),await o.handle.writeFile(i)}}catch(t){throw await this.invalidateHandle(e).catch(()=>void 0),new m(`Failed to append to stream \"${e}\"`,{cause:t})}let s=this.byteIndexes.get(e);return s&&s.tailSeq===n&&this.recordIndexPoint(e,n,o.tailByteOffset,a),o.tailByteOffset+=a,o.lastUsedAt=Date.now(),this.seqCounters.set(e,n+1),r}async openResidentHandle(e){let t=Date.now();for(let[n,r]of this.handles)n!==e&&t-r.lastUsedAt>6e4&&(this.handles.delete(n),await r.handle.close().catch(()=>void 0));for(;this.handles.size>=8;){let e,t=1/0;for(let[n,r]of this.handles)r.lastUsedAt<t&&(t=r.lastUsedAt,e=n);if(e===void 0)break;let n=this.handles.get(e);this.handles.delete(e),await n.handle.close().catch(()=>void 0)}let n=await r(this.resolvePath(e),U),{size:i}=await n.stat(),a={handle:n,lastUsedAt:t,tailByteOffset:i};return this.handles.set(e,a),a}async invalidateHandle(e){let t=this.handles.get(e);t&&(this.handles.delete(e),await t.handle.close().catch(()=>void 0))}recordIndexPoint(e,t,n,r){let i=this.byteIndexes.get(e);!i||t<i.tailSeq||(t%G===0&&q(i,{seq:t,byteOffset:n}),i.tailSeq=t+1,i.tailByteOffset=n+r)}ensureByteIndex(e){let t=this.byteIndexes.get(e);return t||(t={anchors:[],tailSeq:0,tailByteOffset:0},this.byteIndexes.set(e,t)),t}nearestAnchor(e,t){let n={seq:0,byteOffset:0};for(let r of e.anchors)if(r.seq<=t&&r.seq>=n.seq)n=r;else if(r.seq>t)break;return n}async repairPartialTail(e){await this.invalidateHandle(e);let t=this.resolvePath(e),n;try{n=await r(t,`r+`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let{size:e}=await n.stat();if(e===0)return;let t=Buffer.alloc(1);if(await n.read(t,0,1,e-1),t[0]===10)return;let r=-1,i=e;for(;i>0&&r===-1;){let e=Math.max(0,i-65536),t=Buffer.alloc(i-e);await n.read(t,0,t.length,e);for(let n=t.length-1;n>=0;n--)if(t[n]===10){r=e+n+1;break}i=e}await n.truncate(r===-1?0:r)}catch(t){throw new m(`Failed to repair stream \"${e}\"`,{cause:t})}finally{await n.close()}}async*read(e,t){let n=t?.from??0,i=t?.to??1/0;if(i<=n)return;let a;try{a=await r(this.resolvePath(e),`r`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let t=this.ensureByteIndex(e),r=n<t.tailSeq?this.nearestAnchor(t,n):{seq:t.tailSeq,byteOffset:t.tailByteOffset},o=Buffer.alloc(W),s=new p(`utf-8`),c=``,l=r.seq,u=r.byteOffset,d=r.byteOffset;for(;;){let r=d,{bytesRead:f}=await a.read(o,0,W,d);if(f===0)break;d+=f;let p=0;for(;l<n&&l<t.tailSeq&&p<f;){let e=o.subarray(p,f).indexOf(10);if(e===-1){p=f;break}p+=e+1,l++,u=r+p}if(p>=f)continue;c+=s.write(o.subarray(p,f));let m;for(;(m=c.indexOf(`\n`))>=0;){let r=c.slice(0,m),a=u+Buffer.byteLength(r,`utf-8`)+1;if(c=c.slice(m+1),r.length===0){u=a;continue}if(l>=t.tailSeq&&(l%G===0&&q(t,{seq:l,byteOffset:u}),t.tailSeq=l+1,t.tailByteOffset=a),l>=n&&(yield{seq:l,streamId:e,entry:this.codec.decode(r)}),l++,u=a,l>=i)return}}}finally{await a.close()}}async count(e){let t=this.seqCounters.get(e);if(t!==void 0)return t;let n=await this.countLines(e);return this.seqCounters.set(e,n),n}async*tail(e){let t=this.resolvePath(e),n=0,i=0,a=``;for(;!this.closed;){let o;try{o=await r(t,`r`)}catch(t){if(Z(t)){await X(this.pollIntervalMs);continue}throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let{size:t}=await o.stat();if(t<i&&(i=0,n=0,a=``),t>i){let r=t-i,s=Buffer.alloc(r);await o.read(s,0,r,i),i=t,a+=s.toString(`utf-8`);let c;for(;(c=a.indexOf(`\n`))>=0;){let t=a.slice(0,c);a=a.slice(c+1),t.length!==0&&(yield{seq:n,streamId:e,entry:this.codec.decode(t)},n++)}}}finally{await o.close()}if(this.closed)break;await X(this.pollIntervalMs)}}async close(){this.closed=!0,await this.writeQueue.catch(()=>void 0);for(let[,e]of this.handles)await e.handle.close().catch(()=>void 0);this.handles.clear()}truncate(e,t){if(t<=0)return this.clear(e);let n=this.writeQueue.then(()=>this.truncateSerial(e,t));return this.writeQueue=n.catch(()=>void 0),n}async truncateSerial(e,t){await this.invalidateHandle(e);let n;try{n=await r(this.resolvePath(e),`r+`)}catch(t){if(Z(t))return;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let e=Buffer.alloc(W),r=0,i=0,a=-1;outer:for(;;){let{bytesRead:o}=await n.read(e,0,W,null);if(o===0)break;for(let n=0;n<o;n++)if(e[n]===10&&(r++,r===t)){a=i+n+1;break outer}i+=o}if(a===-1)return;await n.truncate(a)}catch(t){throw new m(`Failed to truncate stream \"${e}\"`,{cause:t})}finally{await n.close()}this.seqCounters.set(e,t),this.byteIndexes.delete(e)}clear(e){let t=this.writeQueue.then(()=>this.clearSerial(e));return this.writeQueue=t.catch(()=>void 0),t}async clearSerial(e){await this.invalidateHandle(e),this.seqCounters.delete(e),this.byteIndexes.delete(e);try{await c(this.resolvePath(e))}catch(t){if(!Z(t))throw new m(`Failed to clear stream \"${e}\"`,{cause:t})}}async countLines(e){let t;try{t=await r(this.resolvePath(e),`r`)}catch(t){if(Z(t))return 0;throw new m(`Failed to read stream \"${e}\"`,{cause:t})}try{let e=Buffer.alloc(W),n=0;for(;;){let{bytesRead:r}=await t.read(e,0,W,null);if(r===0)break;for(let t=0;t<r;t++)e[t]===10&&n++}return n}finally{await t.close()}}async nextSeq(e){let t=this.seqCounters.get(e);if(t!==void 0)return t;let n=await this.countLines(e);return this.seqCounters.set(e,n),n}resolvePath(e){return t(this.baseDir,`${encodeURIComponent(e)}${this.extension}`)}async ensureDir(){this.dirReady||=(await n(this.baseDir,{recursive:!0}),!0)}};function ce(e){switch(e.type){case`memory`:return new J;case`file`:return new Y({baseDir:e.baseDir,extension:e.extension,codec:e.codec,pollIntervalMs:e.pollIntervalMs,writerLock:e.writerLock});default:throw Error(`Unsupported append-log type: ${e.type}`)}}var le=class{subs=new Map;publish(e){let t=this.subs.get(e.streamId);if(t)for(let n of[...t])n(e)}subscribe(e,t){let n=this.subs.get(e);return n||this.subs.set(e,n=new Set),n.add(t),()=>{let n=this.subs.get(e);n&&(n.delete(t),n.size===0&&this.subs.delete(e))}}};function ue(e){let t=new le;return{store:{async append(n,r){let i=await e.append(n,r);return t.publish(i),i},read:(t,n)=>e.read(t,n),tail:t=>e.tail(t),truncate:(t,n)=>e.truncate(t,n),clear:t=>e.clear(t),...e.count?{count:t=>e.count(t)}:{},...e.close?{close:()=>e.close()}:{}},live:t}}function X(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function Z(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}function Q(e){let t=e;return typeof t.getChangeLog==`function`&&typeof t.getChangesSince==`function`&&typeof t.getDeletedSince==`function`}const de=[`sessions`];var $=class{namespaces;backend;now;instances=new Map;appendLogs=new Map;constructor(e){this.namespaces=new Set(e.namespaces),this.backend=e.backend,this.now=e.now??(()=>Date.now())}has(e){return this.namespaces.has(e)}listNamespaces(){return[...this.namespaces]}wsId(e,t){return`${e?.trim()||`default`}/${t}`}async appendEntry(e,t,n,r){if(!this.namespaces.has(e))throw Error(`Unknown storage namespace: ${e}`);let i=this.appendLogs.get(e);return i||(i=new J,this.appendLogs.set(e,i)),i.append(this.wsId(t,n),r)}async save(e,t,n,r,i){await S(this.resolve(e),this.wsId(t,n),r,{now:this.now(),owner:i,updatedBy:i})}async updateMetadata(e,t,n,r,i){let a=this.resolve(e),o=await a.load(this.wsId(t,n));if(!o)throw Error(`Document \"${n}\" not found in \"${e}\"`);o.metadata={...o.metadata,...r},i&&(o.metadata.updatedBy=i),await a.save(o)}async load(e,t,n){let r=await this.resolve(e).load(this.wsId(t,n));return r?r.data:null}async delete(e,t,n){return this.resolve(e).delete(this.wsId(t,n))}async list(e,t,n,r){let i=await this.resolve(e).list(),a=`${t||`default`}/`,o=[];for(let e of i)e.startsWith(a)&&o.push({clean:e.slice(a.length),prefixed:e});let s=o.map(e=>e.clean);if(!n||!r||r===`all`)return s;let c=[];for(let{clean:t,prefixed:i}of o){let a=await this.resolve(e).load(i);if(!a)continue;let o=a.metadata;(r===`owned`&&o.owner===n||r===`visible`&&(o.owner===n||o.visibility===`shared`||!o.visibility))&&c.push(t)}return c}async getChangesSince(e,t,n,r){let i=this.resolve(e),a=t?`${t}/`:`default/`;if(Q(i)){let t=r?.map(e=>`${a}${e}`),o=i.getChangesSince(e,n,t),s=new Map;for(let e of o.ids){if(!e.startsWith(a))continue;let t=e.slice(a.length),n=await i.load(e);s.set(t,n?.data??null)}let c=i.getDeletedSince(e,n);for(let e of c){if(!e.startsWith(a))continue;let t=e.slice(a.length);s.has(t)||s.set(t,null)}return{documents:s,cursor:o.cursor}}let o=await i.list(),s=new Map,c=n;for(let e of o){if(!e.startsWith(a))continue;let t=e.slice(a.length);if(r&&!r.includes(t))continue;let o=await i.load(e);o&&o.metadata.updatedAt>n&&(s.set(t,o.data),c=Math.max(c,o.metadata.updatedAt))}return{documents:s,cursor:c}}async listPaginated(e,t,n){let r=this.resolve(e),i=await r.list(),a=`${t||`default`}/`,o=new Map;for(let e of i)e.startsWith(a)&&o.set(e,e.slice(a.length));let s=[];for(let[e,t]of o){let n=await r.load(e);n&&s.push({...n,id:t})}let c=n.order===`asc`?1:-1;return s.sort((e,t)=>c*(this.sortValue(e,n.sortBy)-this.sortValue(t,n.sortBy))),y(s.map(e=>e.id),{page:n.page,pageSize:n.pageSize})}sortValue(e,t){let n=e.metadata[t];if(typeof n==`number`)return n;let r=e.data?.metadata?.[t];return typeof r==`number`?r:e.metadata.updatedAt}async*streamEvents(e,t,n,r=500,i){if(!this.namespaces.has(e))return;let a=this.resolve(e);if(!Q(a))return;let o=n,s=-1;for(;!i?.aborted&&(await pe(r,i),!i?.aborted);){let n=a.getChangeLog(e,o),r=`${t||`default`}/`;for(let e of n){if(i?.aborted)break;e.id<=s||e.doc_id.startsWith(r)&&(yield{...e,doc_id:e.doc_id.slice(r.length)},o=Math.max(o,e.ts),s=Math.max(s,e.id))}}}resolve(e){if(!this.namespaces.has(e))throw Error(`Unknown storage namespace: ${e}`);let t=this.instances.get(e);return t||(t=this.backend(e),this.instances.set(e,t)),t}};function fe(e){return new $({namespaces:e.namespaces??de,backend:n=>new j({baseDir:t(e.baseDir,n)}),now:e.now})}function pe(e,t){return new Promise(n=>{if(t?.aborted){n();return}let r=setTimeout(n,e);t&&t.addEventListener(`abort`,()=>{clearTimeout(r),n()},{once:!0})})}var me=class{baseUrl;getAuth;getHeaders;fetchImpl;timeoutMs;pollIntervalMs;flushAtSize;codec;buffer=new Map;flushingStreams=new Set;flushTimer;closed=!1;constructor(e){this.baseUrl=e.baseUrl.replace(/\\/+$/,``),this.getAuth=e.getAuth,this.getHeaders=e.getHeaders??(async()=>({})),this.fetchImpl=e.fetch??globalThis.fetch,this.timeoutMs=e.timeoutMs??3e4,this.pollIntervalMs=e.pollIntervalMs??500,this.flushAtSize=e.flushAtSize??100,this.codec=e.codec??{encode:e=>JSON.stringify(e),decode:e=>JSON.parse(e)};let t=e.flushIntervalMs??2e3;this.flushTimer=setInterval(()=>{this.flushAll().catch(()=>{})},t),this.flushTimer.unref?.()}async append(e,t){let n=this.codec.encode(t);return new Promise((t,r)=>{if(this.closed){r(Error(`HttpAppendLog closed — buffered events lost`));return}let i=this.buffer.get(e)??[];i.push({encoded:n,resolve:t,reject:r}),this.buffer.set(e,i),i.length>=this.flushAtSize&&this.tryFlush(e).catch(()=>{})})}async tryFlush(e){if(this.flushingStreams.has(e))return;let t=this.buffer.get(e);if(!(!t||t.length===0)){this.flushingStreams.add(e);try{await this.flushOne(e,t)}finally{this.flushingStreams.delete(e)}}}async flushOne(e,t){this.buffer.delete(e);try{let{seqs:n}=await this.request(`/trace/${encodeURIComponent(e)}`,`POST`,t.map(e=>e.encoded));for(let r=0;r<t.length;r++){let i=n[r]??-1,a=this.codec.decode(t[r].encoded);t[r].resolve({seq:i,streamId:e,entry:a})}}catch{if(this.closed)for(let e of t)e.reject(Error(`HttpAppendLog closed — buffered events lost`));else{let n=this.buffer.get(e)??[];this.buffer.set(e,[...n,...t])}}}async flushAll(){let e=[...this.buffer.keys()];for(let t of e){if(this.flushingStreams.has(t))continue;let e=this.buffer.get(t);if(e&&e.length>0){this.flushingStreams.add(t);try{await this.flushOne(t,e)}finally{this.flushingStreams.delete(t)}}}}async*read(e,t){let n=new URLSearchParams;t?.from!==void 0&&n.set(`from`,String(t.from)),t?.to!==void 0&&n.set(`to`,String(t.to));let r=n.toString(),i=`/trace/${encodeURIComponent(e)}${r?`?${r}`:``}`,{events:a}=await this.request(i,`GET`);for(let n=0;n<a.length;n++){let r=this.codec.decode(a[n]);yield{seq:(t?.from??0)+n,streamId:e,entry:r}}}async*tail(e){let t=-1;{let n=`/trace/${encodeURIComponent(e)}`,{events:r}=await this.request(n,`GET`);for(let n=0;n<r.length;n++){let i=this.codec.decode(r[n]),a=n;t=a,yield{seq:a,streamId:e,entry:i}}}for(;!this.closed;){let n=t+1,r=`/trace/${encodeURIComponent(e)}?from=${n}`;try{let{events:i}=await this.request(r,`GET`);for(let r=0;r<i.length;r++){let a=this.codec.decode(i[r]),o=n+r;t=o,yield{seq:o,streamId:e,entry:a}}}catch{}await he(this.pollIntervalMs)}}async count(e){let{count:t}=await this.request(`/trace/${encodeURIComponent(e)}/count`,`GET`);return t+(this.buffer.get(e)?.length??0)}async truncate(e,t){await this.request(`/trace/${encodeURIComponent(e)}/truncate`,`POST`,{toSeq:t})}async clear(e){await this.request(`/trace/${encodeURIComponent(e)}`,`DELETE`)}async close(){this.closed=!0,this.flushTimer&&=(clearInterval(this.flushTimer),void 0),await this.flushAll();let e=Date.now()+this.timeoutMs+5e3;for(;this.flushingStreams.size>0&&Date.now()<e;)await new Promise(e=>setTimeout(e,50));await this.flushAll()}async request(e,t,n){let r=await this.getAuth(),i={...await this.getHeaders(),...r.token?{Authorization:`Bearer ${r.token}`}:{},Accept:`application/json`};n!==void 0&&(i[`Content-Type`]=`application/json`);let a=new AbortController,o=setTimeout(()=>a.abort(),this.timeoutMs),s=`${this.baseUrl}${e}`;try{let e=await this.fetchImpl(s,{method:t,headers:i,body:n===void 0?void 0:JSON.stringify(n),signal:a.signal});if(!e.ok)throw Error(`HTTP ${e.status}: ${e.statusText}`);return await e.json()}finally{clearTimeout(o)}}};function he(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}export{N as Database,N as default,te as DiskPanelStatePersistence,re as DiskPasteStatePersistence,C as DiskPersistence,Y as FileAppendLog,j as FilePersistence,me as HttpAppendLog,J as MemoryAppendLog,x as MemoryPersistence,b as MemoryPersistenceBase,m as PersistenceError,ie as RemotePersistence,h as RemotePersistenceError,g as RemoteSnapshotConflictError,V as SqlitePersistence,$ as StorageHost,z as acquireDb,_ as atomicWriteFile,ce as createAppendLog,fe as createFileStorageHost,ne as createInMemoryPanelStatePersistence,A as createInMemoryPasteStatePersistence,ue as createLiveTappedAppendLog,H as createPersistence,k as normalizePasteSnapshot,v as pageBounds,y as paginate,B as releaseDb,O as resolvePasteNextId,S as saveDocument};\n//# sourceMappingURL=index.js.map","import { Database } from '@x-otto/persistence'\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 close(): void\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 // RFC-354 P3-1:`loadRow` 按 (id, userId) 过滤,所以\"读不到\"有两种含义——\n // ①该 id 确实不存在;②该 id 存在但**属于另一个用户**。此前两种情况都直接\n // 返回 `{ok:true, revision:1}`,而 `INSERT OR IGNORE` 在情况②会撞主键静默\n // no-op → 路由回 200 + ETag,但数据从未落盘、该用户也读不回来(实证:A 的\n // 行纹丝不动,B 写入彻底丢失却收到成功回执)。\n //\n // 用 `changes` 区分:0 = 主键已被他人占用,属归属冲突而非并发修改,回 false\n // 让路由走 409。不返回 403 是因为本层不应泄露\"该 id 被谁占用\"的存在性信息,\n // 且路由层已有 409 通道(`currentRevision` 对该用户同样返回 undefined,\n // 客户端据此可知不是简单的 revision 落后)。\n const result = stmts.insert.run(id, data, userId ?? null, now, now)\n if (result.changes === 0) {\n return { ok: false as const }\n }\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, session_id, seq)——不同用户的同名 session 互不可见。\n // SQLite 不支持 ALTER TABLE 改主键——旧表需重建。\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 // 旧表无 user_id 列——重建表结构(数据迁移:旧数据 user_id='' 保留,历史 trace 仍可被\n // userId='' 的请求访问——多租户启用前的数据不会丢失,只是不属于任何新租户)。\n db.exec('ALTER TABLE trace_events RENAME TO trace_events_old')\n db.exec(`\n CREATE TABLE 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 db.exec(`\n INSERT INTO trace_events (user_id, session_id, seq, data, ts)\n SELECT '', session_id, seq, data, ts FROM trace_events_old\n `)\n db.exec('DROP TABLE trace_events_old')\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 const appendTxn = db.transaction((userId: string, sessionId: string, data: string, now: number): number => {\n // RFC-353 S11: INSERT 的子查询 MAX(seq)+1 与随后单独的 SELECT MAX(seq) 之间存在\n // 理论竞态窗口——若本 store 未来被多连接/多进程共享(当前是单进程单连接,better-sqlite3\n // 同步 API 天然串行化,无实际竞态),INSERT 提交后另一连接可能在本连接读 lastSeq 前\n // 插入更高的 seq,导致 append() 返回值不是\"本次 append 真正分配到的 seq\"。\n // 显式事务包裹 INSERT+SELECT,使两者对其他连接呈现为一个原子操作,消除该窗口,\n // 不依赖\"当前部署形态恰好是单连接\"这一隐含假设。\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 return {\n append(userId: string, sessionId: string, data: string): number {\n const now = Date.now()\n return appendTxn(userId, sessionId, data, now)\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 close(): void {\n db.close()\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 { paginate } from '@x-otto/persistence'\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 // RFC-372 S8:此前路由恒返回 `{ ids }`,但客户端 `RemotePersistence.listPaginated()`\n // 期望 `PaginatedResult<string>` 形状(items/total/page/pageSize/totalPages/hasNext/\n // hasPrevious)——两者完全不匹配,是既存的契约缺口。现在返回完整 PaginatedResult\n // 形状,使客户端能正确解析分页语义。query 参数此前被完全忽略,现在消费 page/\n // pageSize 做内存分页(ids 全量返回后切片,数据量级可控)。\n //\n // F12:0-based page 契约对齐(client 原样发送 0=第一页)。用显式 Number.isFinite\n // 判断而非 `|| 0`——`parseInt('0')||1` 会把合法首页 0 误判为缺省值升成 1,导致\n // page=0 与 page=1 都被压成 1、第二页永远拿第一页(本次一并修复)。\n const parsedPage = Number.parseInt(c.req.query('page') ?? '', 10)\n const parsedPageSize = Number.parseInt(c.req.query('pageSize') ?? '', 10)\n const page = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0\n const pageSize = Number.isFinite(parsedPageSize) && parsedPageSize > 0 ? parsedPageSize : 50\n\n // sortBy/order:`store.list()` 返回的 ids 是 string[],本身无 per-id 排序键;其顺序由\n // 存储层固定为 updated_at DESC。因此 order=desc 保持该顺序、order=asc 反转为\n // updated_at ASC。sortBy 无法选择 createdAt 等其它键(存储层不暴露),接受后忽略并\n // 回退 updated_at 语义——记录为语义缺口,而非静默错序。\n const order = c.req.query('order') === 'asc' ? 'asc' : 'desc'\n const ordered = order === 'asc' ? [...ids].reverse() : ids\n\n return c.json(paginate(ordered, { page, pageSize }))\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 userLogin: ReturnType<typeof createUserLoginStore>\n /** 统一关停:先关 HTTP server,再关三个 SQLite 连接。 */\n close(): void\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 userLogin,\n close(): void {\n server.close()\n store.close()\n traceStore.close()\n userLogin.close()\n },\n })\n })\n server.once('error', (err) => {\n store.close()\n traceStore.close()\n userLogin.close()\n reject(err)\n })\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-otto/remote-persistence-server",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/better-sqlite3": "^7.6.12",
|
|
28
28
|
"vitest": "^3.0.0",
|
|
29
|
-
"@x-otto/persistence": "0.0.1-alpha.
|
|
29
|
+
"@x-otto/persistence": "0.0.1-alpha.4"
|
|
30
30
|
},
|
|
31
31
|
"private": false,
|
|
32
32
|
"scripts": {
|