@hexis-ai/engram-server 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/memory-key-store.js +9 -11
- package/dist/adapters/memory.d.ts +1 -0
- package/dist/adapters/memory.js +11 -1
- package/dist/adapters/postgres-key-store.js +5 -11
- package/dist/adapters/postgres.d.ts +1 -0
- package/dist/adapters/postgres.js +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/key-store.d.ts +29 -0
- package/dist/key-store.js +26 -0
- package/dist/openapi.d.ts +8 -0
- package/dist/openapi.js +368 -0
- package/dist/routes/sessions.d.ts +5 -4
- package/dist/routes/sessions.js +12 -4
- package/dist/server.js +1 -0
- package/dist/storage.d.ts +8 -0
- package/openapi.json +1048 -0
- package/package.json +5 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deriveKey, hashKey, mintKey, resolveWorkspaceId, } from "../key-store";
|
|
2
2
|
/**
|
|
3
3
|
* In-process KeyStore for tests and single-node dev. Volatile.
|
|
4
4
|
*/
|
|
@@ -7,9 +7,7 @@ export class InMemoryKeyStore {
|
|
|
7
7
|
keys = new Map();
|
|
8
8
|
byHash = new Map();
|
|
9
9
|
async createWorkspace(input) {
|
|
10
|
-
const id = input
|
|
11
|
-
if (!isValidWorkspaceId(id))
|
|
12
|
-
throw new Error("invalid_workspace_id");
|
|
10
|
+
const id = resolveWorkspaceId(input);
|
|
13
11
|
const existing = this.workspaces.get(id);
|
|
14
12
|
if (existing)
|
|
15
13
|
return existing;
|
|
@@ -40,12 +38,12 @@ export class InMemoryKeyStore {
|
|
|
40
38
|
async issueKey(workspaceId, opts = {}) {
|
|
41
39
|
if (!this.workspaces.has(workspaceId))
|
|
42
40
|
throw new Error("workspace_not_found");
|
|
43
|
-
const raw =
|
|
41
|
+
const { id, hash, prefix, raw } = mintKey();
|
|
44
42
|
const row = {
|
|
45
|
-
id
|
|
43
|
+
id,
|
|
46
44
|
workspaceId,
|
|
47
|
-
keyHash:
|
|
48
|
-
prefix
|
|
45
|
+
keyHash: hash,
|
|
46
|
+
prefix,
|
|
49
47
|
...(opts.name !== undefined ? { name: opts.name } : {}),
|
|
50
48
|
createdAt: new Date().toISOString(),
|
|
51
49
|
};
|
|
@@ -80,14 +78,14 @@ export class InMemoryKeyStore {
|
|
|
80
78
|
async registerKey(workspaceId, rawKey, name) {
|
|
81
79
|
if (!this.workspaces.has(workspaceId))
|
|
82
80
|
throw new Error("workspace_not_found");
|
|
83
|
-
const hash =
|
|
81
|
+
const { id, hash, prefix } = deriveKey(rawKey);
|
|
84
82
|
if (this.byHash.has(hash))
|
|
85
83
|
return;
|
|
86
84
|
const row = {
|
|
87
|
-
id
|
|
85
|
+
id,
|
|
88
86
|
workspaceId,
|
|
89
87
|
keyHash: hash,
|
|
90
|
-
prefix
|
|
88
|
+
prefix,
|
|
91
89
|
...(name !== undefined ? { name } : {}),
|
|
92
90
|
createdAt: new Date().toISOString(),
|
|
93
91
|
};
|
|
@@ -20,6 +20,7 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
20
20
|
}): Promise<void>;
|
|
21
21
|
appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
|
|
22
22
|
getSession(sessionId: string): Promise<Session | null>;
|
|
23
|
+
getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
|
|
23
24
|
listSessions(opts: {
|
|
24
25
|
limit: number;
|
|
25
26
|
channel?: string;
|
package/dist/adapters/memory.js
CHANGED
|
@@ -62,6 +62,12 @@ export class InMemoryAdapter {
|
|
|
62
62
|
return null;
|
|
63
63
|
return foldEvents(s.row, [...s.events.values()], new Date());
|
|
64
64
|
}
|
|
65
|
+
async getSessionEvents(sessionId) {
|
|
66
|
+
const s = this.sessions.get(sessionId);
|
|
67
|
+
if (!s)
|
|
68
|
+
return null;
|
|
69
|
+
return [...s.events.values()].sort((a, b) => a.seq - b.seq);
|
|
70
|
+
}
|
|
65
71
|
async listSessions(opts) {
|
|
66
72
|
const now = new Date();
|
|
67
73
|
const all = [];
|
|
@@ -104,7 +110,11 @@ export class InMemoryAdapter {
|
|
|
104
110
|
const existing = this.persons.get(id);
|
|
105
111
|
const next = {
|
|
106
112
|
id,
|
|
107
|
-
|
|
113
|
+
// `PersonCreate` has no notion of "clear the name" — that is
|
|
114
|
+
// `updatePerson`'s job. A missing value (and, defensively, an
|
|
115
|
+
// explicit null that bypasses the schema) means "keep what's
|
|
116
|
+
// there", matching the Postgres adapter's `COALESCE(EXCLUDED.…)`.
|
|
117
|
+
display_name: input.display_name != null
|
|
108
118
|
? input.display_name
|
|
109
119
|
: existing?.display_name ?? null,
|
|
110
120
|
created_at: existing?.created_at ?? now,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deriveKey, hashKey, mintKey, resolveWorkspaceId, } from "../key-store";
|
|
2
2
|
import { runMigrations } from "../migrator";
|
|
3
3
|
export class PostgresKeyStore {
|
|
4
4
|
sql;
|
|
@@ -13,9 +13,7 @@ export class PostgresKeyStore {
|
|
|
13
13
|
await runMigrations(this.sql);
|
|
14
14
|
}
|
|
15
15
|
async createWorkspace(input) {
|
|
16
|
-
const id = input
|
|
17
|
-
if (!isValidWorkspaceId(id))
|
|
18
|
-
throw new Error("invalid_workspace_id");
|
|
16
|
+
const id = resolveWorkspaceId(input);
|
|
19
17
|
await this.sql `
|
|
20
18
|
INSERT INTO engram_workspaces (id, name, metadata)
|
|
21
19
|
VALUES (${id}, ${input.name ?? null}, ${(input.metadata ?? {})})
|
|
@@ -49,10 +47,7 @@ export class PostgresKeyStore {
|
|
|
49
47
|
const ws = await this.getWorkspace(workspaceId);
|
|
50
48
|
if (!ws)
|
|
51
49
|
throw new Error("workspace_not_found");
|
|
52
|
-
const raw =
|
|
53
|
-
const id = crypto.randomUUID();
|
|
54
|
-
const hash = hashKey(raw);
|
|
55
|
-
const prefix = keyPrefix(raw);
|
|
50
|
+
const { id, hash, prefix, raw } = mintKey();
|
|
56
51
|
const rows = await this.sql `
|
|
57
52
|
INSERT INTO engram_api_keys (id, workspace_id, key_hash, prefix, name)
|
|
58
53
|
VALUES (${id}, ${workspaceId}, ${hash}, ${prefix}, ${opts.name ?? null})
|
|
@@ -110,11 +105,10 @@ export class PostgresKeyStore {
|
|
|
110
105
|
const ws = await this.getWorkspace(workspaceId);
|
|
111
106
|
if (!ws)
|
|
112
107
|
throw new Error("workspace_not_found");
|
|
113
|
-
const hash =
|
|
114
|
-
const id = crypto.randomUUID();
|
|
108
|
+
const { id, hash, prefix } = deriveKey(rawKey);
|
|
115
109
|
await this.sql `
|
|
116
110
|
INSERT INTO engram_api_keys (id, workspace_id, key_hash, prefix, name)
|
|
117
|
-
VALUES (${id}, ${workspaceId}, ${hash}, ${
|
|
111
|
+
VALUES (${id}, ${workspaceId}, ${hash}, ${prefix}, ${name ?? null})
|
|
118
112
|
ON CONFLICT (key_hash) DO NOTHING
|
|
119
113
|
`;
|
|
120
114
|
}
|
|
@@ -37,6 +37,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
37
37
|
}): Promise<void>;
|
|
38
38
|
appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
|
|
39
39
|
getSession(sessionId: string): Promise<Session | null>;
|
|
40
|
+
getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
|
|
40
41
|
listSessions(opts: {
|
|
41
42
|
limit: number;
|
|
42
43
|
channel?: string;
|
|
@@ -110,6 +110,21 @@ export class PostgresAdapter {
|
|
|
110
110
|
};
|
|
111
111
|
return foldEvents(row, events.map((e) => e.payload), new Date());
|
|
112
112
|
}
|
|
113
|
+
async getSessionEvents(sessionId) {
|
|
114
|
+
const rows = await this.sql `
|
|
115
|
+
SELECT id FROM engram_sessions
|
|
116
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
117
|
+
LIMIT 1
|
|
118
|
+
`;
|
|
119
|
+
if (rows.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
const events = await this.sql `
|
|
122
|
+
SELECT payload FROM engram_events
|
|
123
|
+
WHERE workspace_id = ${this.workspaceId} AND session_id = ${sessionId}
|
|
124
|
+
ORDER BY seq
|
|
125
|
+
`;
|
|
126
|
+
return events.map((e) => e.payload);
|
|
127
|
+
}
|
|
113
128
|
async listSessions(opts) {
|
|
114
129
|
const channelFilter = opts.channel ?? null;
|
|
115
130
|
const rows = await this.sql `
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { type KeyStore, type Workspace, type ApiKeyInfo, type IssuedKey, type Ke
|
|
|
7
7
|
export { InMemoryKeyStore } from "./adapters/memory-key-store";
|
|
8
8
|
export { PostgresKeyStore } from "./adapters/postgres-key-store";
|
|
9
9
|
export { createAdminRouter, type AdminOptions } from "./admin";
|
|
10
|
+
export { buildOpenApiDocument } from "./openapi";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { generateRawKey, hashKey, keyPrefix, isValidWorkspaceId, } from "./key-s
|
|
|
7
7
|
export { InMemoryKeyStore } from "./adapters/memory-key-store";
|
|
8
8
|
export { PostgresKeyStore } from "./adapters/postgres-key-store";
|
|
9
9
|
export { createAdminRouter } from "./admin";
|
|
10
|
+
export { buildOpenApiDocument } from "./openapi";
|
package/dist/key-store.d.ts
CHANGED
|
@@ -61,3 +61,32 @@ export declare function generateRawKey(): string;
|
|
|
61
61
|
export declare function hashKey(raw: string): string;
|
|
62
62
|
export declare function keyPrefix(raw: string): string;
|
|
63
63
|
export declare function isValidWorkspaceId(id: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the workspace id for a `createWorkspace` call: fall back to a random
|
|
66
|
+
* UUID when none is supplied, then validate. Shared by every KeyStore so the
|
|
67
|
+
* id policy can't drift between the in-memory and Postgres adapters.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveWorkspaceId(input: {
|
|
70
|
+
id?: string;
|
|
71
|
+
}): string;
|
|
72
|
+
/** Derived material for a single API key row. */
|
|
73
|
+
export interface KeyMaterial {
|
|
74
|
+
/** Row id (primary key). */
|
|
75
|
+
id: string;
|
|
76
|
+
/** SHA-256 hash of the raw key — the only form that gets persisted. */
|
|
77
|
+
hash: string;
|
|
78
|
+
/** Human-readable prefix shown in listings. */
|
|
79
|
+
prefix: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mint a brand-new random key. Returns the persistable material plus the
|
|
83
|
+
* plaintext `raw` key, which the caller must return to the user exactly once.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mintKey(): KeyMaterial & {
|
|
86
|
+
raw: string;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Derive persistable material for a caller-supplied raw key (`registerKey`).
|
|
90
|
+
* Unlike `mintKey`, the raw key already exists and is not returned.
|
|
91
|
+
*/
|
|
92
|
+
export declare function deriveKey(raw: string): KeyMaterial;
|
package/dist/key-store.js
CHANGED
|
@@ -15,3 +15,29 @@ export function keyPrefix(raw) {
|
|
|
15
15
|
export function isValidWorkspaceId(id) {
|
|
16
16
|
return WORKSPACE_ID_RE.test(id);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the workspace id for a `createWorkspace` call: fall back to a random
|
|
20
|
+
* UUID when none is supplied, then validate. Shared by every KeyStore so the
|
|
21
|
+
* id policy can't drift between the in-memory and Postgres adapters.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveWorkspaceId(input) {
|
|
24
|
+
const id = input.id ?? crypto.randomUUID();
|
|
25
|
+
if (!isValidWorkspaceId(id))
|
|
26
|
+
throw new Error("invalid_workspace_id");
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mint a brand-new random key. Returns the persistable material plus the
|
|
31
|
+
* plaintext `raw` key, which the caller must return to the user exactly once.
|
|
32
|
+
*/
|
|
33
|
+
export function mintKey() {
|
|
34
|
+
const raw = generateRawKey();
|
|
35
|
+
return { id: crypto.randomUUID(), hash: hashKey(raw), prefix: keyPrefix(raw), raw };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive persistable material for a caller-supplied raw key (`registerKey`).
|
|
39
|
+
* Unlike `mintKey`, the raw key already exists and is not returned.
|
|
40
|
+
*/
|
|
41
|
+
export function deriveKey(raw) {
|
|
42
|
+
return { id: crypto.randomUUID(), hash: hashKey(raw), prefix: keyPrefix(raw) };
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Json = Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* Build the OpenAPI 3.1 document. Pure and deterministic — `gen-openapi.ts`
|
|
4
|
+
* writes its output to `openapi.json`, and `openapi.test.ts` asserts the
|
|
5
|
+
* committed file still matches.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildOpenApiDocument(): Json;
|
|
8
|
+
export {};
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.1 document for the engram HTTP API, assembled from the same Zod
|
|
3
|
+
* schemas the server validates with (`src/schemas.ts`). The request-body
|
|
4
|
+
* contract therefore cannot drift from what the server actually accepts —
|
|
5
|
+
* a sync test (`test/openapi.test.ts`) fails the build if it does.
|
|
6
|
+
*
|
|
7
|
+
* This is the language-neutral contract artifact. Non-TypeScript engram SDK
|
|
8
|
+
* ports generate their wire types from `packages/server/openapi.json`
|
|
9
|
+
* (produced by `bun run gen:openapi`) instead of hand-mirroring the
|
|
10
|
+
* TypeScript interfaces in `@hexis-ai/engram-sdk`.
|
|
11
|
+
*
|
|
12
|
+
* `info.version` is the API version (the `/v1` surface), not the npm package
|
|
13
|
+
* version — it changes only when the wire contract changes.
|
|
14
|
+
*
|
|
15
|
+
* The human-readable strings (`summary`, `description`, response and
|
|
16
|
+
* parameter descriptions) are written in Japanese — the API reference is
|
|
17
|
+
* read by the Hexis team. Identifiers, paths, schema names and status
|
|
18
|
+
* codes stay as-is.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { createWorkspaceSchema, eventBatchSchema, issueKeySchema, personCreateSchema, personUpdateSchema, searchRequestSchema, sessionInitSchema, } from "./schemas";
|
|
22
|
+
/** Convert a Zod schema to a JSON Schema object for an OpenAPI component. */
|
|
23
|
+
function toComponent(schema) {
|
|
24
|
+
const js = z.toJSONSchema(schema);
|
|
25
|
+
delete js.$schema;
|
|
26
|
+
return js;
|
|
27
|
+
}
|
|
28
|
+
const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
29
|
+
const jsonBody = (schemaName, required = true) => ({
|
|
30
|
+
required,
|
|
31
|
+
content: { "application/json": { schema: ref(schemaName) } },
|
|
32
|
+
});
|
|
33
|
+
const pathParam = (name, description) => ({
|
|
34
|
+
name,
|
|
35
|
+
in: "path",
|
|
36
|
+
required: true,
|
|
37
|
+
schema: { type: "string" },
|
|
38
|
+
description,
|
|
39
|
+
});
|
|
40
|
+
const queryParam = (name, description, schema = { type: "string" }) => ({ name, in: "query", required: false, schema, description });
|
|
41
|
+
const res = (description) => ({ description });
|
|
42
|
+
/** Default security: a workspace API key. `/admin/v1` paths override this. */
|
|
43
|
+
const workspaceAuth = [{ workspaceKey: [] }];
|
|
44
|
+
const adminAuth = [{ adminToken: [] }];
|
|
45
|
+
// ---------------------------------------------------------------------
|
|
46
|
+
// Tags — group the rendered reference (Scalar, on monet-web /docs/engram)
|
|
47
|
+
// into a per-domain sidebar instead of one flat list of 19 operations.
|
|
48
|
+
//
|
|
49
|
+
// The tag *definitions* (sidebar order + Japanese descriptions) live
|
|
50
|
+
// in TAG_DEFS; `tagged()` stamps a tag onto every operation in a
|
|
51
|
+
// path-item, so each path in buildPaths() declares its tag once,
|
|
52
|
+
// inline. test/openapi.test.ts guards that no operation slips through
|
|
53
|
+
// untagged and no operation references an undefined tag.
|
|
54
|
+
// ---------------------------------------------------------------------
|
|
55
|
+
/** Every tag, in sidebar order, with a Japanese description. */
|
|
56
|
+
const TAG_DEFS = [
|
|
57
|
+
{ name: "Me", description: "識別プローブ" },
|
|
58
|
+
{ name: "Sessions", description: "セッションの作成・取得・イベント追加" },
|
|
59
|
+
{ name: "Search", description: "ワークスペースコーパスへのスコアリング検索" },
|
|
60
|
+
{ name: "Persons", description: "person の作成・更新・検索" },
|
|
61
|
+
{
|
|
62
|
+
name: "Workspaces (admin)",
|
|
63
|
+
description: "ワークスペースの管理(管理者トークン必須)",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "API Keys (admin)",
|
|
67
|
+
description: "ワークスペース API キーの発行・無効化(管理者トークン必須)",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Stamp `tag` onto every operation in a path-item. The path declares its
|
|
72
|
+
* tag once — `tagged("Sessions", { post: {…}, get: {…} })` — rather than
|
|
73
|
+
* repeating `tags` on each method.
|
|
74
|
+
*/
|
|
75
|
+
function tagged(tag, pathItem) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [method, op] of Object.entries(pathItem)) {
|
|
78
|
+
out[method] = { ...op, tags: [tag] };
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
const limitParam = queryParam("limit", "ページサイズ。サーバー側で上限が適用される。", {
|
|
83
|
+
type: "integer",
|
|
84
|
+
minimum: 1,
|
|
85
|
+
});
|
|
86
|
+
const channelParam = queryParam("channel", "セッションのチャンネルで絞り込む。");
|
|
87
|
+
function buildPaths() {
|
|
88
|
+
return {
|
|
89
|
+
"/v1/me": tagged("Me", {
|
|
90
|
+
get: {
|
|
91
|
+
summary: "識別プローブ — 呼び出し元のキーが解決するワークスペースを返す。",
|
|
92
|
+
responses: {
|
|
93
|
+
"200": res("ワークスペース id"),
|
|
94
|
+
"401": res("認証エラー"),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
"/v1/sessions": tagged("Sessions", {
|
|
99
|
+
post: {
|
|
100
|
+
summary: "セッションを作成する。",
|
|
101
|
+
requestBody: jsonBody("SessionInit"),
|
|
102
|
+
responses: {
|
|
103
|
+
"200": res("作成されたセッション id"),
|
|
104
|
+
"400": res("リクエストボディが不正"),
|
|
105
|
+
"401": res("認証エラー"),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
get: {
|
|
109
|
+
summary: "最近のセッション一覧と、それに紐づく persons マップを取得する。",
|
|
110
|
+
parameters: [limitParam, channelParam],
|
|
111
|
+
responses: {
|
|
112
|
+
"200": res("セッション一覧のエンベロープ"),
|
|
113
|
+
"401": res("認証エラー"),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
"/v1/sessions/{id}": tagged("Sessions", {
|
|
118
|
+
get: {
|
|
119
|
+
summary: "単一セッションと、それに紐づく persons マップを取得する。",
|
|
120
|
+
parameters: [pathParam("id", "セッション id。")],
|
|
121
|
+
responses: {
|
|
122
|
+
"200": res("セッションのエンベロープ"),
|
|
123
|
+
"404": res("セッションが見つからない"),
|
|
124
|
+
"401": res("認証エラー"),
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
"/v1/sessions/{id}/events": tagged("Sessions", {
|
|
129
|
+
post: {
|
|
130
|
+
summary: "セッションにイベントを追加する。",
|
|
131
|
+
parameters: [pathParam("id", "セッション id。")],
|
|
132
|
+
requestBody: jsonBody("EventBatch"),
|
|
133
|
+
responses: {
|
|
134
|
+
"204": res("追加完了"),
|
|
135
|
+
"400": res("リクエストボディが不正"),
|
|
136
|
+
"404": res("セッションが見つからない"),
|
|
137
|
+
"401": res("認証エラー"),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
get: {
|
|
141
|
+
summary: "セッションの生イベントログを seq 順で取得する(fold せず、各イベントの時刻つき)。",
|
|
142
|
+
parameters: [pathParam("id", "セッション id。")],
|
|
143
|
+
responses: {
|
|
144
|
+
"200": res("イベントログ"),
|
|
145
|
+
"404": res("セッションが見つからない"),
|
|
146
|
+
"401": res("認証エラー"),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
"/v1/search": tagged("Search", {
|
|
151
|
+
post: {
|
|
152
|
+
summary: "クエリセッションに対してワークスペースのコーパスをスコアリングする。",
|
|
153
|
+
requestBody: jsonBody("SearchRequest"),
|
|
154
|
+
responses: {
|
|
155
|
+
"200": res("スコアリング結果と persons マップ"),
|
|
156
|
+
"400": res("リクエストボディが不正"),
|
|
157
|
+
"404": res("クエリセッションが見つからない"),
|
|
158
|
+
"401": res("認証エラー"),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
"/v1/persons": tagged("Persons", {
|
|
163
|
+
post: {
|
|
164
|
+
summary: "person を作成する(id はサーバーが採番する)。",
|
|
165
|
+
requestBody: jsonBody("PersonCreate"),
|
|
166
|
+
responses: {
|
|
167
|
+
"201": res("作成された person"),
|
|
168
|
+
"400": res("リクエストボディが不正"),
|
|
169
|
+
"401": res("認証エラー"),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
get: {
|
|
173
|
+
summary: "person の一覧取得、またはフリーテキスト検索を行う。",
|
|
174
|
+
parameters: [
|
|
175
|
+
limitParam,
|
|
176
|
+
queryParam("q", "id と display_name に対するフリーテキストクエリ。"),
|
|
177
|
+
],
|
|
178
|
+
responses: {
|
|
179
|
+
"200": res("person 一覧"),
|
|
180
|
+
"401": res("認証エラー"),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
"/v1/persons/{id}": tagged("Persons", {
|
|
185
|
+
put: {
|
|
186
|
+
summary: "呼び出し元が指定した id で person を upsert する。",
|
|
187
|
+
parameters: [pathParam("id", "person id。")],
|
|
188
|
+
requestBody: jsonBody("PersonCreate"),
|
|
189
|
+
responses: {
|
|
190
|
+
"200": res("upsert された person"),
|
|
191
|
+
"400": res("リクエストボディが不正"),
|
|
192
|
+
"401": res("認証エラー"),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
patch: {
|
|
196
|
+
summary: "person のプロフィール項目を部分更新する。",
|
|
197
|
+
parameters: [pathParam("id", "person id。")],
|
|
198
|
+
requestBody: jsonBody("PersonUpdate"),
|
|
199
|
+
responses: {
|
|
200
|
+
"200": res("更新された person"),
|
|
201
|
+
"400": res("リクエストボディが不正"),
|
|
202
|
+
"404": res("person が見つからない"),
|
|
203
|
+
"401": res("認証エラー"),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
get: {
|
|
207
|
+
summary: "単一の person を取得する。",
|
|
208
|
+
parameters: [pathParam("id", "person id。")],
|
|
209
|
+
responses: {
|
|
210
|
+
"200": res("person"),
|
|
211
|
+
"404": res("person が見つからない"),
|
|
212
|
+
"401": res("認証エラー"),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
"/v1/persons/{id}/sessions": tagged("Persons", {
|
|
217
|
+
get: {
|
|
218
|
+
summary: "この person が参加している(または閲覧可能な)セッション一覧。",
|
|
219
|
+
parameters: [
|
|
220
|
+
pathParam("id", "person id。"),
|
|
221
|
+
limitParam,
|
|
222
|
+
channelParam,
|
|
223
|
+
queryParam("scope", "`participant`(デフォルト)または `viewable`。", {
|
|
224
|
+
type: "string",
|
|
225
|
+
enum: ["participant", "viewable"],
|
|
226
|
+
}),
|
|
227
|
+
],
|
|
228
|
+
responses: {
|
|
229
|
+
"200": res("セッション一覧のエンベロープ"),
|
|
230
|
+
"401": res("認証エラー"),
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
"/admin/v1/workspaces": tagged("Workspaces (admin)", {
|
|
235
|
+
post: {
|
|
236
|
+
summary: "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
|
|
237
|
+
security: adminAuth,
|
|
238
|
+
requestBody: jsonBody("CreateWorkspace"),
|
|
239
|
+
responses: {
|
|
240
|
+
"200": res("ワークスペース(issueKey=false でない限りキーも含む)"),
|
|
241
|
+
"400": res("リクエストボディまたはワークスペース id が不正"),
|
|
242
|
+
"401": res("認証エラー"),
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
get: {
|
|
246
|
+
summary: "全ワークスペースを一覧取得する。",
|
|
247
|
+
security: adminAuth,
|
|
248
|
+
responses: {
|
|
249
|
+
"200": res("ワークスペース一覧"),
|
|
250
|
+
"401": res("認証エラー"),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
"/admin/v1/workspaces/{id}": tagged("Workspaces (admin)", {
|
|
255
|
+
get: {
|
|
256
|
+
summary: "単一のワークスペースを取得する。",
|
|
257
|
+
security: adminAuth,
|
|
258
|
+
parameters: [pathParam("id", "ワークスペース id。")],
|
|
259
|
+
responses: {
|
|
260
|
+
"200": res("ワークスペース"),
|
|
261
|
+
"404": res("ワークスペースが見つからない"),
|
|
262
|
+
"401": res("認証エラー"),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
delete: {
|
|
266
|
+
summary: "ワークスペースを削除する(キー・セッション・イベントにカスケードする)。",
|
|
267
|
+
security: adminAuth,
|
|
268
|
+
parameters: [pathParam("id", "ワークスペース id。")],
|
|
269
|
+
responses: {
|
|
270
|
+
"204": res("削除完了"),
|
|
271
|
+
"404": res("ワークスペースが見つからない"),
|
|
272
|
+
"401": res("認証エラー"),
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
"/admin/v1/workspaces/{id}/keys": tagged("API Keys (admin)", {
|
|
277
|
+
post: {
|
|
278
|
+
summary: "ワークスペースに新しい API キーを発行する。",
|
|
279
|
+
security: adminAuth,
|
|
280
|
+
parameters: [pathParam("id", "ワークスペース id。")],
|
|
281
|
+
requestBody: jsonBody("IssueKey", false),
|
|
282
|
+
responses: {
|
|
283
|
+
"200": res("発行されたキー(生のキーは一度のみ返却)"),
|
|
284
|
+
"400": res("リクエストボディが不正"),
|
|
285
|
+
"404": res("ワークスペースが見つからない"),
|
|
286
|
+
"401": res("認証エラー"),
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
get: {
|
|
290
|
+
summary: "ワークスペースの API キー一覧を取得する(ハッシュのみ)。",
|
|
291
|
+
security: adminAuth,
|
|
292
|
+
parameters: [pathParam("id", "ワークスペース id。")],
|
|
293
|
+
responses: {
|
|
294
|
+
"200": res("キー一覧"),
|
|
295
|
+
"404": res("ワークスペースが見つからない"),
|
|
296
|
+
"401": res("認証エラー"),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
"/admin/v1/workspaces/{id}/keys/{keyId}": tagged("API Keys (admin)", {
|
|
301
|
+
delete: {
|
|
302
|
+
summary: "API キーを無効化する。",
|
|
303
|
+
security: adminAuth,
|
|
304
|
+
parameters: [
|
|
305
|
+
pathParam("id", "ワークスペース id。"),
|
|
306
|
+
pathParam("keyId", "キー id。"),
|
|
307
|
+
],
|
|
308
|
+
responses: {
|
|
309
|
+
"204": res("無効化完了(冪等)"),
|
|
310
|
+
"404": res("キーが見つからない"),
|
|
311
|
+
"401": res("認証エラー"),
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Build the OpenAPI 3.1 document. Pure and deterministic — `gen-openapi.ts`
|
|
319
|
+
* writes its output to `openapi.json`, and `openapi.test.ts` asserts the
|
|
320
|
+
* committed file still matches.
|
|
321
|
+
*/
|
|
322
|
+
export function buildOpenApiDocument() {
|
|
323
|
+
return {
|
|
324
|
+
openapi: "3.1.0",
|
|
325
|
+
info: {
|
|
326
|
+
title: "engram-server",
|
|
327
|
+
version: "1.0",
|
|
328
|
+
description: "AI エージェント向けのクロスセッション検索。リクエストボディは" +
|
|
329
|
+
"サーバーの Zod スキーマ(src/schemas.ts)から導出される。" +
|
|
330
|
+
"レスポンススキーマは現状ステータスコードのみの記述で、" +
|
|
331
|
+
"本格的な記述は今後対応予定。",
|
|
332
|
+
},
|
|
333
|
+
tags: TAG_DEFS.map((t) => ({ ...t })),
|
|
334
|
+
servers: [
|
|
335
|
+
{ url: "/", description: "デプロイされた engram-server からの相対パス" },
|
|
336
|
+
],
|
|
337
|
+
security: workspaceAuth,
|
|
338
|
+
components: {
|
|
339
|
+
securitySchemes: {
|
|
340
|
+
workspaceKey: {
|
|
341
|
+
type: "http",
|
|
342
|
+
scheme: "bearer",
|
|
343
|
+
description: "ワークスペース API キー(`eng_…`)。`X-Api-Key` ヘッダーでも受け付ける。",
|
|
344
|
+
},
|
|
345
|
+
adminToken: {
|
|
346
|
+
type: "apiKey",
|
|
347
|
+
in: "header",
|
|
348
|
+
name: "X-Admin-Token",
|
|
349
|
+
description: "`/admin/v1/*` 用のプラットフォーム管理者トークン。" +
|
|
350
|
+
"`Authorization: Bearer` でも受け付ける。",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
schemas: {
|
|
354
|
+
SessionInit: toComponent(sessionInitSchema),
|
|
355
|
+
// `EventBatch` inlines the per-event shape. A standalone `SessionEvent`
|
|
356
|
+
// component (cross-`$ref`'d) is a planned follow-up alongside response
|
|
357
|
+
// schemas — zod's `toJSONSchema` inlines by default.
|
|
358
|
+
EventBatch: toComponent(eventBatchSchema),
|
|
359
|
+
PersonCreate: toComponent(personCreateSchema),
|
|
360
|
+
PersonUpdate: toComponent(personUpdateSchema),
|
|
361
|
+
SearchRequest: toComponent(searchRequestSchema),
|
|
362
|
+
CreateWorkspace: toComponent(createWorkspaceSchema),
|
|
363
|
+
IssueKey: toComponent(issueKeySchema),
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
paths: buildPaths(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -3,9 +3,10 @@ import type { Env } from "../context";
|
|
|
3
3
|
import { type RouteConfig } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Session routes. Mount under `/v1`:
|
|
6
|
-
* POST /v1/sessions
|
|
7
|
-
* POST /v1/sessions/:id/events
|
|
8
|
-
* GET /v1/sessions/:id
|
|
9
|
-
* GET /v1/sessions
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* POST /v1/sessions/:id/events append events
|
|
8
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
9
|
+
* GET /v1/sessions/:id/events fetch the raw, ordered event log
|
|
10
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
10
11
|
*/
|
|
11
12
|
export declare function sessionsRoutes(cfg: RouteConfig): Hono<Env>;
|
package/dist/routes/sessions.js
CHANGED
|
@@ -3,10 +3,11 @@ import { eventBatchSchema, parseJsonBody, sessionInitSchema } from "../schemas";
|
|
|
3
3
|
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Session routes. Mount under `/v1`:
|
|
6
|
-
* POST /v1/sessions
|
|
7
|
-
* POST /v1/sessions/:id/events
|
|
8
|
-
* GET /v1/sessions/:id
|
|
9
|
-
* GET /v1/sessions
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* POST /v1/sessions/:id/events append events
|
|
8
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
9
|
+
* GET /v1/sessions/:id/events fetch the raw, ordered event log
|
|
10
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
10
11
|
*/
|
|
11
12
|
export function sessionsRoutes(cfg) {
|
|
12
13
|
const app = new Hono();
|
|
@@ -40,6 +41,13 @@ export function sessionsRoutes(cfg) {
|
|
|
40
41
|
const persons = await resolvePersonMap(c.var.ctx.storage, [s]);
|
|
41
42
|
return c.json({ session: s, persons });
|
|
42
43
|
});
|
|
44
|
+
app.get("/sessions/:id/events", async (c) => {
|
|
45
|
+
const id = c.req.param("id");
|
|
46
|
+
const events = await c.var.ctx.storage.getSessionEvents(id);
|
|
47
|
+
if (events === null)
|
|
48
|
+
return c.json({ error: "session_not_found" }, 404);
|
|
49
|
+
return c.json({ events });
|
|
50
|
+
});
|
|
43
51
|
app.get("/sessions", async (c) => {
|
|
44
52
|
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
45
53
|
const channel = c.req.query("channel") || undefined;
|