@hexis-ai/engram-server 0.4.0 → 0.6.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.d.ts +12 -1
- package/dist/adapters/memory.js +70 -0
- package/dist/adapters/postgres.d.ts +8 -1
- package/dist/adapters/postgres.js +147 -0
- package/dist/migrations/0002-aliases.d.ts +2 -0
- package/dist/migrations/0002-aliases.js +30 -0
- package/dist/migrations/0003-identities.d.ts +2 -0
- package/dist/migrations/0003-identities.js +33 -0
- package/dist/migrations/index.js +7 -1
- package/dist/openapi.js +62 -0
- package/dist/routes/identities.d.ts +14 -0
- package/dist/routes/identities.js +33 -0
- package/dist/routes/persons.d.ts +8 -6
- package/dist/routes/persons.js +30 -7
- package/dist/schemas.d.ts +15 -0
- package/dist/schemas.js +23 -0
- package/dist/server.js +8 -1
- package/dist/storage.d.ts +31 -1
- package/openapi.json +181 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
3
3
|
import { type StorageAdapter } from "../storage";
|
|
4
4
|
export interface InMemoryAdapterOptions {
|
|
5
5
|
/** Override for tests. Default: `p_${random}` with 8 chars. */
|
|
@@ -12,6 +12,10 @@ export interface InMemoryAdapterOptions {
|
|
|
12
12
|
export declare class InMemoryAdapter implements StorageAdapter {
|
|
13
13
|
private readonly sessions;
|
|
14
14
|
private readonly persons;
|
|
15
|
+
/** Keyed by `${personId} ${name.toLowerCase()}` — see `aliasKey` below. */
|
|
16
|
+
private readonly aliases;
|
|
17
|
+
/** Keyed by ref (e.g. `slack:U12345`). */
|
|
18
|
+
private readonly identities;
|
|
15
19
|
private readonly newPersonId;
|
|
16
20
|
constructor(opts?: InMemoryAdapterOptions);
|
|
17
21
|
createSession(init: SessionInit & {
|
|
@@ -39,4 +43,11 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
39
43
|
limit: number;
|
|
40
44
|
q?: string;
|
|
41
45
|
}): Promise<PersonInfo[]>;
|
|
46
|
+
upsertAlias(personId: string, input: {
|
|
47
|
+
name: string;
|
|
48
|
+
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
49
|
+
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
50
|
+
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
51
|
+
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
52
|
+
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
42
53
|
}
|
package/dist/adapters/memory.js
CHANGED
|
@@ -13,6 +13,10 @@ function defaultPersonId() {
|
|
|
13
13
|
export class InMemoryAdapter {
|
|
14
14
|
sessions = new Map();
|
|
15
15
|
persons = new Map();
|
|
16
|
+
/** Keyed by `${personId} ${name.toLowerCase()}` — see `aliasKey` below. */
|
|
17
|
+
aliases = new Map();
|
|
18
|
+
/** Keyed by ref (e.g. `slack:U12345`). */
|
|
19
|
+
identities = new Map();
|
|
16
20
|
newPersonId;
|
|
17
21
|
constructor(opts = {}) {
|
|
18
22
|
this.newPersonId = opts.newPersonId ?? defaultPersonId;
|
|
@@ -158,4 +162,70 @@ export class InMemoryAdapter {
|
|
|
158
162
|
matched.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
159
163
|
return matched.slice(0, opts.limit);
|
|
160
164
|
}
|
|
165
|
+
// --- Aliases ------------------------------------------------------
|
|
166
|
+
async upsertAlias(personId, input) {
|
|
167
|
+
if (!this.persons.has(personId))
|
|
168
|
+
return null;
|
|
169
|
+
const key = aliasKey(personId, input.name);
|
|
170
|
+
const now = new Date().toISOString();
|
|
171
|
+
const existing = this.aliases.get(key);
|
|
172
|
+
const increment = input.increment ?? true;
|
|
173
|
+
if (existing && !increment) {
|
|
174
|
+
// Idempotent backfill: keep the existing row as-is.
|
|
175
|
+
return existing;
|
|
176
|
+
}
|
|
177
|
+
const next = {
|
|
178
|
+
person_id: personId,
|
|
179
|
+
name: existing?.name ?? input.name,
|
|
180
|
+
caller: input.caller,
|
|
181
|
+
usage_count: existing ? existing.usage_count + 1 : 1,
|
|
182
|
+
last_used: input.last_used,
|
|
183
|
+
created_at: existing?.created_at ?? now,
|
|
184
|
+
updated_at: now,
|
|
185
|
+
};
|
|
186
|
+
this.aliases.set(key, next);
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
189
|
+
async listAliases(personId) {
|
|
190
|
+
const matches = [...this.aliases.values()].filter((a) => a.person_id === personId);
|
|
191
|
+
matches.sort((a, b) => b.last_used.localeCompare(a.last_used));
|
|
192
|
+
return matches;
|
|
193
|
+
}
|
|
194
|
+
// --- Identities ---------------------------------------------------
|
|
195
|
+
async upsertIdentity(ref, input) {
|
|
196
|
+
if (!this.persons.has(input.person_id))
|
|
197
|
+
return null;
|
|
198
|
+
const now = new Date().toISOString();
|
|
199
|
+
const existing = this.identities.get(ref);
|
|
200
|
+
const next = {
|
|
201
|
+
ref,
|
|
202
|
+
person_id: input.person_id,
|
|
203
|
+
service: input.service,
|
|
204
|
+
external_id: input.external_id,
|
|
205
|
+
display_name: input.display_name !== undefined
|
|
206
|
+
? input.display_name
|
|
207
|
+
: existing?.display_name ?? null,
|
|
208
|
+
source: input.source !== undefined ? input.source : existing?.source ?? null,
|
|
209
|
+
is_primary: input.is_primary !== undefined
|
|
210
|
+
? input.is_primary
|
|
211
|
+
: existing?.is_primary ?? null,
|
|
212
|
+
picture: input.picture !== undefined ? input.picture : existing?.picture ?? null,
|
|
213
|
+
linked_at: input.linked_at,
|
|
214
|
+
created_at: existing?.created_at ?? now,
|
|
215
|
+
updated_at: now,
|
|
216
|
+
};
|
|
217
|
+
this.identities.set(ref, next);
|
|
218
|
+
return next;
|
|
219
|
+
}
|
|
220
|
+
async getIdentityByRef(ref) {
|
|
221
|
+
return this.identities.get(ref) ?? null;
|
|
222
|
+
}
|
|
223
|
+
async listIdentitiesByPerson(personId) {
|
|
224
|
+
const matches = [...this.identities.values()].filter((i) => i.person_id === personId);
|
|
225
|
+
matches.sort((a, b) => b.linked_at.localeCompare(a.linked_at));
|
|
226
|
+
return matches;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function aliasKey(personId, name) {
|
|
230
|
+
return `${personId}${name.toLowerCase()}`;
|
|
161
231
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
3
3
|
import { type StorageAdapter } from "../storage";
|
|
4
4
|
/**
|
|
5
5
|
* Minimal subset of `postgres` driver's tagged-template surface that this
|
|
@@ -56,4 +56,11 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
56
56
|
limit: number;
|
|
57
57
|
q?: string;
|
|
58
58
|
}): Promise<PersonInfo[]>;
|
|
59
|
+
upsertAlias(personId: string, input: {
|
|
60
|
+
name: string;
|
|
61
|
+
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
62
|
+
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
63
|
+
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
64
|
+
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
65
|
+
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
59
66
|
}
|
|
@@ -237,6 +237,117 @@ export class PostgresAdapter {
|
|
|
237
237
|
`;
|
|
238
238
|
return rows.map(toPersonInfo);
|
|
239
239
|
}
|
|
240
|
+
// --- Aliases ------------------------------------------------------
|
|
241
|
+
async upsertAlias(personId, input) {
|
|
242
|
+
// Pre-check rather than rely on the FK so unknown persons return
|
|
243
|
+
// `null` instead of throwing a constraint violation.
|
|
244
|
+
const personExists = await this.sql `
|
|
245
|
+
SELECT id FROM engram_persons
|
|
246
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${personId}
|
|
247
|
+
LIMIT 1
|
|
248
|
+
`;
|
|
249
|
+
if (personExists.length === 0)
|
|
250
|
+
return null;
|
|
251
|
+
const increment = input.increment ?? true;
|
|
252
|
+
// Branch on the upsert behaviour. `increment=true` bumps usage_count
|
|
253
|
+
// and replaces caller/last_used; `increment=false` is idempotent —
|
|
254
|
+
// a no-op when the row already exists.
|
|
255
|
+
const rows = increment
|
|
256
|
+
? await this.sql `
|
|
257
|
+
INSERT INTO engram_aliases (workspace_id, person_id, name, caller, usage_count, last_used)
|
|
258
|
+
VALUES (
|
|
259
|
+
${this.workspaceId}, ${personId}, ${input.name},
|
|
260
|
+
${input.caller}, 1, ${input.last_used}
|
|
261
|
+
)
|
|
262
|
+
ON CONFLICT (workspace_id, person_id, name_lower) DO UPDATE SET
|
|
263
|
+
usage_count = engram_aliases.usage_count + 1,
|
|
264
|
+
caller = EXCLUDED.caller,
|
|
265
|
+
last_used = EXCLUDED.last_used,
|
|
266
|
+
updated_at = now()
|
|
267
|
+
RETURNING person_id, name, caller, usage_count, last_used, created_at, updated_at
|
|
268
|
+
`
|
|
269
|
+
: await this.sql `
|
|
270
|
+
INSERT INTO engram_aliases (workspace_id, person_id, name, caller, usage_count, last_used)
|
|
271
|
+
VALUES (
|
|
272
|
+
${this.workspaceId}, ${personId}, ${input.name},
|
|
273
|
+
${input.caller}, 1, ${input.last_used}
|
|
274
|
+
)
|
|
275
|
+
ON CONFLICT (workspace_id, person_id, name_lower) DO UPDATE SET
|
|
276
|
+
-- No-op update so RETURNING still produces a row.
|
|
277
|
+
updated_at = engram_aliases.updated_at
|
|
278
|
+
RETURNING person_id, name, caller, usage_count, last_used, created_at, updated_at
|
|
279
|
+
`;
|
|
280
|
+
return toAliasInfo(rows[0]);
|
|
281
|
+
}
|
|
282
|
+
async listAliases(personId) {
|
|
283
|
+
const rows = await this.sql `
|
|
284
|
+
SELECT person_id, name, caller, usage_count, last_used, created_at, updated_at
|
|
285
|
+
FROM engram_aliases
|
|
286
|
+
WHERE workspace_id = ${this.workspaceId} AND person_id = ${personId}
|
|
287
|
+
ORDER BY last_used DESC
|
|
288
|
+
`;
|
|
289
|
+
return rows.map(toAliasInfo);
|
|
290
|
+
}
|
|
291
|
+
// --- Identities ---------------------------------------------------
|
|
292
|
+
async upsertIdentity(ref, input) {
|
|
293
|
+
// Pre-check rather than rely on the FK so unknown persons return
|
|
294
|
+
// `null` instead of throwing a constraint violation.
|
|
295
|
+
const personExists = await this.sql `
|
|
296
|
+
SELECT id FROM engram_persons
|
|
297
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${input.person_id}
|
|
298
|
+
LIMIT 1
|
|
299
|
+
`;
|
|
300
|
+
if (personExists.length === 0)
|
|
301
|
+
return null;
|
|
302
|
+
const rows = await this.sql `
|
|
303
|
+
INSERT INTO engram_identities (
|
|
304
|
+
workspace_id, ref, person_id, service, external_id,
|
|
305
|
+
display_name, source, is_primary, picture, linked_at
|
|
306
|
+
)
|
|
307
|
+
VALUES (
|
|
308
|
+
${this.workspaceId}, ${ref}, ${input.person_id},
|
|
309
|
+
${input.service}, ${input.external_id},
|
|
310
|
+
${input.display_name ?? null}, ${input.source ?? null},
|
|
311
|
+
${input.is_primary ?? null}, ${input.picture ?? null},
|
|
312
|
+
${input.linked_at}
|
|
313
|
+
)
|
|
314
|
+
ON CONFLICT (workspace_id, ref) DO UPDATE SET
|
|
315
|
+
person_id = EXCLUDED.person_id,
|
|
316
|
+
service = EXCLUDED.service,
|
|
317
|
+
external_id = EXCLUDED.external_id,
|
|
318
|
+
display_name = COALESCE(EXCLUDED.display_name, engram_identities.display_name),
|
|
319
|
+
source = COALESCE(EXCLUDED.source, engram_identities.source),
|
|
320
|
+
is_primary = COALESCE(EXCLUDED.is_primary, engram_identities.is_primary),
|
|
321
|
+
picture = COALESCE(EXCLUDED.picture, engram_identities.picture),
|
|
322
|
+
linked_at = EXCLUDED.linked_at,
|
|
323
|
+
updated_at = now()
|
|
324
|
+
RETURNING ref, person_id, service, external_id, display_name, source,
|
|
325
|
+
is_primary, picture, linked_at, created_at, updated_at
|
|
326
|
+
`;
|
|
327
|
+
return toIdentityInfo(rows[0]);
|
|
328
|
+
}
|
|
329
|
+
async getIdentityByRef(ref) {
|
|
330
|
+
const rows = await this.sql `
|
|
331
|
+
SELECT ref, person_id, service, external_id, display_name, source,
|
|
332
|
+
is_primary, picture, linked_at, created_at, updated_at
|
|
333
|
+
FROM engram_identities
|
|
334
|
+
WHERE workspace_id = ${this.workspaceId} AND ref = ${ref}
|
|
335
|
+
LIMIT 1
|
|
336
|
+
`;
|
|
337
|
+
if (rows.length === 0)
|
|
338
|
+
return null;
|
|
339
|
+
return toIdentityInfo(rows[0]);
|
|
340
|
+
}
|
|
341
|
+
async listIdentitiesByPerson(personId) {
|
|
342
|
+
const rows = await this.sql `
|
|
343
|
+
SELECT ref, person_id, service, external_id, display_name, source,
|
|
344
|
+
is_primary, picture, linked_at, created_at, updated_at
|
|
345
|
+
FROM engram_identities
|
|
346
|
+
WHERE workspace_id = ${this.workspaceId} AND person_id = ${personId}
|
|
347
|
+
ORDER BY linked_at DESC
|
|
348
|
+
`;
|
|
349
|
+
return rows.map(toIdentityInfo);
|
|
350
|
+
}
|
|
240
351
|
}
|
|
241
352
|
function toPersonInfo(r) {
|
|
242
353
|
const toIso = (v) => (typeof v === "string" ? v : v.toISOString());
|
|
@@ -247,3 +358,39 @@ function toPersonInfo(r) {
|
|
|
247
358
|
updated_at: toIso(r.updated_at),
|
|
248
359
|
};
|
|
249
360
|
}
|
|
361
|
+
function toAliasInfo(r) {
|
|
362
|
+
const toIso = (v) => (typeof v === "string" ? v : v.toISOString());
|
|
363
|
+
// last_used is a DATE column; postgres-js returns it as a Date at UTC
|
|
364
|
+
// midnight. Render as plain YYYY-MM-DD to match the wire type.
|
|
365
|
+
const lastUsed = typeof r.last_used === "string"
|
|
366
|
+
? r.last_used.slice(0, 10)
|
|
367
|
+
: r.last_used.toISOString().slice(0, 10);
|
|
368
|
+
return {
|
|
369
|
+
person_id: r.person_id,
|
|
370
|
+
name: r.name,
|
|
371
|
+
caller: r.caller,
|
|
372
|
+
usage_count: r.usage_count,
|
|
373
|
+
last_used: lastUsed,
|
|
374
|
+
created_at: toIso(r.created_at),
|
|
375
|
+
updated_at: toIso(r.updated_at),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function toIdentityInfo(r) {
|
|
379
|
+
const toIso = (v) => (typeof v === "string" ? v : v.toISOString());
|
|
380
|
+
const linkedAt = typeof r.linked_at === "string"
|
|
381
|
+
? r.linked_at.slice(0, 10)
|
|
382
|
+
: r.linked_at.toISOString().slice(0, 10);
|
|
383
|
+
return {
|
|
384
|
+
ref: r.ref,
|
|
385
|
+
person_id: r.person_id,
|
|
386
|
+
service: r.service,
|
|
387
|
+
external_id: r.external_id,
|
|
388
|
+
display_name: r.display_name,
|
|
389
|
+
source: r.source,
|
|
390
|
+
is_primary: r.is_primary,
|
|
391
|
+
picture: r.picture,
|
|
392
|
+
linked_at: linkedAt,
|
|
393
|
+
created_at: toIso(r.created_at),
|
|
394
|
+
updated_at: toIso(r.updated_at),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const name = "0002-aliases";
|
|
2
|
+
export declare const sql = "\n-- Per-person alias history. Mirrors monet's `aliases` table at the\n-- canonical-engram level so identity resolution can move out of monet\n-- once consumers catch up. Keyed by name_lower (case-insensitive,\n-- DB-generated) so case variants collapse.\nCREATE TABLE IF NOT EXISTS engram_aliases (\n workspace_id TEXT NOT NULL,\n person_id TEXT NOT NULL,\n name TEXT NOT NULL,\n name_lower TEXT GENERATED ALWAYS AS (lower(name)) STORED,\n caller TEXT NOT NULL,\n usage_count INTEGER NOT NULL DEFAULT 1,\n last_used DATE NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (workspace_id, person_id, name_lower),\n FOREIGN KEY (workspace_id, person_id)\n REFERENCES engram_persons(workspace_id, id) ON DELETE CASCADE\n);\n\n-- Reverse lookup: \"who answers to this name?\" is the common identity-\n-- resolution query and benefits from an index on the lowercased form.\nCREATE INDEX IF NOT EXISTS idx_engram_aliases_name_lower\n ON engram_aliases (workspace_id, name_lower);\n\n-- Forward lookup: list all of a person's aliases by recency.\nCREATE INDEX IF NOT EXISTS idx_engram_aliases_person_last_used\n ON engram_aliases (workspace_id, person_id, last_used DESC);\n";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const name = "0002-aliases";
|
|
2
|
+
export const sql = `
|
|
3
|
+
-- Per-person alias history. Mirrors monet's \`aliases\` table at the
|
|
4
|
+
-- canonical-engram level so identity resolution can move out of monet
|
|
5
|
+
-- once consumers catch up. Keyed by name_lower (case-insensitive,
|
|
6
|
+
-- DB-generated) so case variants collapse.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS engram_aliases (
|
|
8
|
+
workspace_id TEXT NOT NULL,
|
|
9
|
+
person_id TEXT NOT NULL,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
name_lower TEXT GENERATED ALWAYS AS (lower(name)) STORED,
|
|
12
|
+
caller TEXT NOT NULL,
|
|
13
|
+
usage_count INTEGER NOT NULL DEFAULT 1,
|
|
14
|
+
last_used DATE NOT NULL,
|
|
15
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
16
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
17
|
+
PRIMARY KEY (workspace_id, person_id, name_lower),
|
|
18
|
+
FOREIGN KEY (workspace_id, person_id)
|
|
19
|
+
REFERENCES engram_persons(workspace_id, id) ON DELETE CASCADE
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
-- Reverse lookup: \"who answers to this name?\" is the common identity-
|
|
23
|
+
-- resolution query and benefits from an index on the lowercased form.
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_engram_aliases_name_lower
|
|
25
|
+
ON engram_aliases (workspace_id, name_lower);
|
|
26
|
+
|
|
27
|
+
-- Forward lookup: list all of a person's aliases by recency.
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_engram_aliases_person_last_used
|
|
29
|
+
ON engram_aliases (workspace_id, person_id, last_used DESC);
|
|
30
|
+
`;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const name = "0003-identities";
|
|
2
|
+
export declare const sql = "\n-- External identity \u2192 person mapping. Mirrors monet's `identities`\n-- table at the canonical-engram level so identity resolution\n-- (slack:U12345 \u2192 person id, email:foo@bar \u2192 person id, \u2026) can move\n-- out of monet once consumers catch up.\n--\n-- Ref is workspace-scoped here even though monet's PK is just `ref`;\n-- engram is multi-tenant from the floor up.\nCREATE TABLE IF NOT EXISTS engram_identities (\n workspace_id TEXT NOT NULL,\n ref TEXT NOT NULL,\n person_id TEXT NOT NULL,\n service TEXT NOT NULL,\n external_id TEXT NOT NULL,\n display_name TEXT,\n source TEXT,\n is_primary BOOLEAN,\n picture TEXT,\n linked_at DATE NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (workspace_id, ref),\n FOREIGN KEY (workspace_id, person_id)\n REFERENCES engram_persons(workspace_id, id) ON DELETE CASCADE\n);\n\n-- The two query shapes the resolver needs.\nCREATE INDEX IF NOT EXISTS idx_engram_identities_person\n ON engram_identities (workspace_id, person_id);\nCREATE INDEX IF NOT EXISTS idx_engram_identities_service_external\n ON engram_identities (workspace_id, service, external_id);\n";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const name = "0003-identities";
|
|
2
|
+
export const sql = `
|
|
3
|
+
-- External identity → person mapping. Mirrors monet's \`identities\`
|
|
4
|
+
-- table at the canonical-engram level so identity resolution
|
|
5
|
+
-- (slack:U12345 → person id, email:foo@bar → person id, …) can move
|
|
6
|
+
-- out of monet once consumers catch up.
|
|
7
|
+
--
|
|
8
|
+
-- Ref is workspace-scoped here even though monet's PK is just \`ref\`;
|
|
9
|
+
-- engram is multi-tenant from the floor up.
|
|
10
|
+
CREATE TABLE IF NOT EXISTS engram_identities (
|
|
11
|
+
workspace_id TEXT NOT NULL,
|
|
12
|
+
ref TEXT NOT NULL,
|
|
13
|
+
person_id TEXT NOT NULL,
|
|
14
|
+
service TEXT NOT NULL,
|
|
15
|
+
external_id TEXT NOT NULL,
|
|
16
|
+
display_name TEXT,
|
|
17
|
+
source TEXT,
|
|
18
|
+
is_primary BOOLEAN,
|
|
19
|
+
picture TEXT,
|
|
20
|
+
linked_at DATE NOT NULL,
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
22
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
23
|
+
PRIMARY KEY (workspace_id, ref),
|
|
24
|
+
FOREIGN KEY (workspace_id, person_id)
|
|
25
|
+
REFERENCES engram_persons(workspace_id, id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- The two query shapes the resolver needs.
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_engram_identities_person
|
|
30
|
+
ON engram_identities (workspace_id, person_id);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_engram_identities_service_external
|
|
32
|
+
ON engram_identities (workspace_id, service, external_id);
|
|
33
|
+
`;
|
package/dist/migrations/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as m0001 from "./0001-baseline";
|
|
2
|
+
import * as m0002 from "./0002-aliases";
|
|
3
|
+
import * as m0003 from "./0003-identities";
|
|
2
4
|
/**
|
|
3
5
|
* Schema migrations, applied in array order. Add a new file under
|
|
4
6
|
* `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
|
|
@@ -6,4 +8,8 @@ import * as m0001 from "./0001-baseline";
|
|
|
6
8
|
* EXISTS, ADD COLUMN IF NOT EXISTS, etc.) so a first apply on a DB that
|
|
7
9
|
* predates the migrator is a no-op.
|
|
8
10
|
*/
|
|
9
|
-
export const MIGRATIONS = [
|
|
11
|
+
export const MIGRATIONS = [
|
|
12
|
+
{ name: m0001.name, sql: m0001.sql },
|
|
13
|
+
{ name: m0002.name, sql: m0002.sql },
|
|
14
|
+
{ name: m0003.name, sql: m0003.sql },
|
|
15
|
+
];
|
package/dist/openapi.js
CHANGED
|
@@ -58,6 +58,10 @@ const TAG_DEFS = [
|
|
|
58
58
|
{ name: "Sessions", description: "セッションの作成・取得・イベント追加" },
|
|
59
59
|
{ name: "Search", description: "ワークスペースコーパスへのスコアリング検索" },
|
|
60
60
|
{ name: "Persons", description: "person の作成・更新・検索" },
|
|
61
|
+
{
|
|
62
|
+
name: "Identities",
|
|
63
|
+
description: "外部 ID(slack: / email: など)の resolve と upsert",
|
|
64
|
+
},
|
|
61
65
|
{
|
|
62
66
|
name: "Workspaces (admin)",
|
|
63
67
|
description: "ワークスペースの管理(管理者トークン必須)",
|
|
@@ -231,6 +235,64 @@ function buildPaths() {
|
|
|
231
235
|
},
|
|
232
236
|
},
|
|
233
237
|
}),
|
|
238
|
+
"/v1/persons/{id}/aliases": tagged("Persons", {
|
|
239
|
+
get: {
|
|
240
|
+
summary: "この person の alias 一覧を取得する(直近使用が先頭)。",
|
|
241
|
+
parameters: [pathParam("id", "person id。")],
|
|
242
|
+
responses: {
|
|
243
|
+
"200": res("alias 一覧"),
|
|
244
|
+
"401": res("認証エラー"),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
"/v1/persons/{id}/aliases/{name}": tagged("Persons", {
|
|
249
|
+
put: {
|
|
250
|
+
summary: "person に alias を upsert する。name は case-insensitive で比較される。",
|
|
251
|
+
parameters: [
|
|
252
|
+
pathParam("id", "person id。"),
|
|
253
|
+
pathParam("name", "alias の名前(URL-encoded)。"),
|
|
254
|
+
],
|
|
255
|
+
requestBody: jsonBody("AliasUpsert"),
|
|
256
|
+
responses: {
|
|
257
|
+
"200": res("upsert された alias"),
|
|
258
|
+
"400": res("リクエストボディが不正"),
|
|
259
|
+
"404": res("person が見つからない"),
|
|
260
|
+
"401": res("認証エラー"),
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
"/v1/persons/{id}/identities": tagged("Persons", {
|
|
265
|
+
get: {
|
|
266
|
+
summary: "この person のすべての identity を取得する(直近 link が先頭)。",
|
|
267
|
+
parameters: [pathParam("id", "person id。")],
|
|
268
|
+
responses: {
|
|
269
|
+
"200": res("identity 一覧"),
|
|
270
|
+
"401": res("認証エラー"),
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
"/v1/identities/{ref}": tagged("Identities", {
|
|
275
|
+
put: {
|
|
276
|
+
summary: "ref(例: `slack:U12345`、`email:foo@bar.com`)で identity を upsert する。",
|
|
277
|
+
parameters: [pathParam("ref", "global identity ref(URL-encoded)。")],
|
|
278
|
+
requestBody: jsonBody("IdentityUpsert"),
|
|
279
|
+
responses: {
|
|
280
|
+
"200": res("upsert された identity"),
|
|
281
|
+
"400": res("リクエストボディが不正"),
|
|
282
|
+
"404": res("person_id が見つからない"),
|
|
283
|
+
"401": res("認証エラー"),
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
get: {
|
|
287
|
+
summary: "ref から identity を resolve する。",
|
|
288
|
+
parameters: [pathParam("ref", "global identity ref(URL-encoded)。")],
|
|
289
|
+
responses: {
|
|
290
|
+
"200": res("identity 情報"),
|
|
291
|
+
"404": res("ref が見つからない"),
|
|
292
|
+
"401": res("認証エラー"),
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
234
296
|
"/admin/v1/workspaces": tagged("Workspaces (admin)", {
|
|
235
297
|
post: {
|
|
236
298
|
summary: "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../context";
|
|
3
|
+
import type { RouteConfig } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Identity routes. Mount under `/v1`:
|
|
6
|
+
* PUT /v1/identities/:ref upsert by global ref (e.g. `slack:U12345`)
|
|
7
|
+
* GET /v1/identities/:ref resolve a ref to its identity record
|
|
8
|
+
*
|
|
9
|
+
* The "list this person's identities" endpoint is exposed by the
|
|
10
|
+
* persons routes at `/v1/persons/:id/identities` because that's the
|
|
11
|
+
* natural client mental model — start from a person, fan out — not
|
|
12
|
+
* start from a ref.
|
|
13
|
+
*/
|
|
14
|
+
export declare function identitiesRoutes(_cfg: RouteConfig): Hono<Env>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { identityUpsertSchema, parseJsonBody } from "../schemas";
|
|
3
|
+
/**
|
|
4
|
+
* Identity routes. Mount under `/v1`:
|
|
5
|
+
* PUT /v1/identities/:ref upsert by global ref (e.g. `slack:U12345`)
|
|
6
|
+
* GET /v1/identities/:ref resolve a ref to its identity record
|
|
7
|
+
*
|
|
8
|
+
* The "list this person's identities" endpoint is exposed by the
|
|
9
|
+
* persons routes at `/v1/persons/:id/identities` because that's the
|
|
10
|
+
* natural client mental model — start from a person, fan out — not
|
|
11
|
+
* start from a ref.
|
|
12
|
+
*/
|
|
13
|
+
export function identitiesRoutes(_cfg) {
|
|
14
|
+
const app = new Hono();
|
|
15
|
+
app.put("/identities/:ref", async (c) => {
|
|
16
|
+
const ref = c.req.param("ref");
|
|
17
|
+
const body = await parseJsonBody(c, identityUpsertSchema);
|
|
18
|
+
if (body instanceof Response)
|
|
19
|
+
return body;
|
|
20
|
+
const identity = await c.var.ctx.storage.upsertIdentity(ref, body);
|
|
21
|
+
if (!identity)
|
|
22
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
23
|
+
return c.json(identity);
|
|
24
|
+
});
|
|
25
|
+
app.get("/identities/:ref", async (c) => {
|
|
26
|
+
const ref = c.req.param("ref");
|
|
27
|
+
const identity = await c.var.ctx.storage.getIdentityByRef(ref);
|
|
28
|
+
if (!identity)
|
|
29
|
+
return c.json({ error: "identity_not_found" }, 404);
|
|
30
|
+
return c.json(identity);
|
|
31
|
+
});
|
|
32
|
+
return app;
|
|
33
|
+
}
|
package/dist/routes/persons.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { Env } from "../context";
|
|
|
3
3
|
import { type RouteConfig } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Person routes. Mount under `/v1`:
|
|
6
|
-
* POST /v1/persons
|
|
7
|
-
* PUT /v1/persons/:id
|
|
8
|
-
* PATCH /v1/persons/:id
|
|
9
|
-
* GET /v1/persons/:id
|
|
10
|
-
* GET /v1/persons
|
|
11
|
-
* GET /v1/persons/:id/sessions
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* GET /v1/persons/:id fetch one person
|
|
10
|
+
* GET /v1/persons list / free-text search persons
|
|
11
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
12
|
+
* PUT /v1/persons/:id/aliases/:name upsert an alias for this person
|
|
13
|
+
* GET /v1/persons/:id/aliases list aliases for this person (newest-used-first)
|
|
12
14
|
*/
|
|
13
15
|
export declare function personsRoutes(cfg: RouteConfig): Hono<Env>;
|
package/dist/routes/persons.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { parseJsonBody, personCreateSchema, personUpdateSchema } from "../schemas";
|
|
2
|
+
import { aliasUpsertSchema, parseJsonBody, personCreateSchema, personUpdateSchema, } from "../schemas";
|
|
3
3
|
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Person routes. Mount under `/v1`:
|
|
6
|
-
* POST /v1/persons
|
|
7
|
-
* PUT /v1/persons/:id
|
|
8
|
-
* PATCH /v1/persons/:id
|
|
9
|
-
* GET /v1/persons/:id
|
|
10
|
-
* GET /v1/persons
|
|
11
|
-
* GET /v1/persons/:id/sessions
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* GET /v1/persons/:id fetch one person
|
|
10
|
+
* GET /v1/persons list / free-text search persons
|
|
11
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
12
|
+
* PUT /v1/persons/:id/aliases/:name upsert an alias for this person
|
|
13
|
+
* GET /v1/persons/:id/aliases list aliases for this person (newest-used-first)
|
|
12
14
|
*/
|
|
13
15
|
export function personsRoutes(cfg) {
|
|
14
16
|
const app = new Hono();
|
|
@@ -50,6 +52,27 @@ export function personsRoutes(cfg) {
|
|
|
50
52
|
const persons = await c.var.ctx.storage.listPersons({ limit, q });
|
|
51
53
|
return c.json({ persons });
|
|
52
54
|
});
|
|
55
|
+
app.put("/persons/:id/aliases/:name", async (c) => {
|
|
56
|
+
const id = c.req.param("id");
|
|
57
|
+
const name = c.req.param("name");
|
|
58
|
+
const body = await parseJsonBody(c, aliasUpsertSchema);
|
|
59
|
+
if (body instanceof Response)
|
|
60
|
+
return body;
|
|
61
|
+
const alias = await c.var.ctx.storage.upsertAlias(id, { name, ...body });
|
|
62
|
+
if (!alias)
|
|
63
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
64
|
+
return c.json(alias);
|
|
65
|
+
});
|
|
66
|
+
app.get("/persons/:id/aliases", async (c) => {
|
|
67
|
+
const id = c.req.param("id");
|
|
68
|
+
const aliases = await c.var.ctx.storage.listAliases(id);
|
|
69
|
+
return c.json({ aliases });
|
|
70
|
+
});
|
|
71
|
+
app.get("/persons/:id/identities", async (c) => {
|
|
72
|
+
const id = c.req.param("id");
|
|
73
|
+
const identities = await c.var.ctx.storage.listIdentitiesByPerson(id);
|
|
74
|
+
return c.json({ identities });
|
|
75
|
+
});
|
|
53
76
|
app.get("/persons/:id/sessions", async (c) => {
|
|
54
77
|
const id = c.req.param("id");
|
|
55
78
|
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
package/dist/schemas.d.ts
CHANGED
|
@@ -66,6 +66,21 @@ export declare const personCreateSchema: z.ZodObject<{
|
|
|
66
66
|
export declare const personUpdateSchema: z.ZodObject<{
|
|
67
67
|
display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
68
|
}, z.core.$strip>;
|
|
69
|
+
export declare const aliasUpsertSchema: z.ZodObject<{
|
|
70
|
+
caller: z.ZodString;
|
|
71
|
+
last_used: z.ZodString;
|
|
72
|
+
increment: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
export declare const identityUpsertSchema: z.ZodObject<{
|
|
75
|
+
person_id: z.ZodString;
|
|
76
|
+
service: z.ZodString;
|
|
77
|
+
external_id: z.ZodString;
|
|
78
|
+
display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
|
+
source: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
80
|
+
is_primary: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
81
|
+
picture: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
82
|
+
linked_at: z.ZodString;
|
|
83
|
+
}, z.core.$strip>;
|
|
69
84
|
export declare const searchRequestSchema: z.ZodObject<{
|
|
70
85
|
query: z.ZodUnion<readonly [z.ZodObject<{
|
|
71
86
|
sessionId: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -56,6 +56,29 @@ export const personCreateSchema = z.object({
|
|
|
56
56
|
export const personUpdateSchema = z.object({
|
|
57
57
|
display_name: z.string().nullable().optional(),
|
|
58
58
|
});
|
|
59
|
+
// --- Aliases ----------------------------------------------------------
|
|
60
|
+
export const aliasUpsertSchema = z.object({
|
|
61
|
+
caller: z.string().min(1),
|
|
62
|
+
// YYYY-MM-DD; loose-validate so callers can also pass an ISO timestamp
|
|
63
|
+
// and the server will accept the date prefix.
|
|
64
|
+
last_used: z
|
|
65
|
+
.string()
|
|
66
|
+
.regex(/^\d{4}-\d{2}-\d{2}/, "expected YYYY-MM-DD"),
|
|
67
|
+
increment: z.boolean().optional(),
|
|
68
|
+
});
|
|
69
|
+
// --- Identities -------------------------------------------------------
|
|
70
|
+
export const identityUpsertSchema = z.object({
|
|
71
|
+
person_id: z.string().min(1),
|
|
72
|
+
service: z.string().min(1),
|
|
73
|
+
external_id: z.string().min(1),
|
|
74
|
+
display_name: z.string().nullable().optional(),
|
|
75
|
+
source: z.string().nullable().optional(),
|
|
76
|
+
is_primary: z.boolean().nullable().optional(),
|
|
77
|
+
picture: z.string().nullable().optional(),
|
|
78
|
+
linked_at: z
|
|
79
|
+
.string()
|
|
80
|
+
.regex(/^\d{4}-\d{2}-\d{2}/, "expected YYYY-MM-DD"),
|
|
81
|
+
});
|
|
59
82
|
// --- Search ----------------------------------------------------------
|
|
60
83
|
const searchQuerySchema = z.union([
|
|
61
84
|
z.object({ sessionId: z.string().min(1) }),
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { log, newRequestId } from "./logger";
|
|
|
3
3
|
import { createAdminRouter } from "./admin";
|
|
4
4
|
import { sessionsRoutes } from "./routes/sessions";
|
|
5
5
|
import { personsRoutes } from "./routes/persons";
|
|
6
|
+
import { identitiesRoutes } from "./routes/identities";
|
|
6
7
|
import { searchRoutes } from "./routes/search";
|
|
7
8
|
/**
|
|
8
9
|
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
@@ -58,6 +59,10 @@ export function createServer(opts) {
|
|
|
58
59
|
persons: "POST/GET /v1/persons",
|
|
59
60
|
personById: "GET/PUT/PATCH /v1/persons/:id",
|
|
60
61
|
personSessions: "GET /v1/persons/:id/sessions",
|
|
62
|
+
personAliases: "GET /v1/persons/:id/aliases",
|
|
63
|
+
upsertAlias: "PUT /v1/persons/:id/aliases/:name",
|
|
64
|
+
identityByRef: "GET/PUT /v1/identities/:ref",
|
|
65
|
+
personIdentities: "GET /v1/persons/:id/identities",
|
|
61
66
|
},
|
|
62
67
|
}));
|
|
63
68
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
@@ -77,10 +82,12 @@ export function createServer(opts) {
|
|
|
77
82
|
await next();
|
|
78
83
|
});
|
|
79
84
|
// Identity probe — echoes the workspace the caller's key resolves to.
|
|
80
|
-
// Used by clients (e.g. engram
|
|
85
|
+
// Used by host clients (e.g. monet's `/v1/engram/*` proxy) to label
|
|
86
|
+
// which tenant they're viewing.
|
|
81
87
|
app.get("/v1/me", (c) => c.json({ workspaceId: c.var.ctx.workspaceId }));
|
|
82
88
|
app.route("/v1", sessionsRoutes(cfg));
|
|
83
89
|
app.route("/v1", personsRoutes(cfg));
|
|
90
|
+
app.route("/v1", identitiesRoutes(cfg));
|
|
84
91
|
app.route("/v1", searchRoutes(cfg));
|
|
85
92
|
return app;
|
|
86
93
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
|
|
3
3
|
/**
|
|
4
4
|
* Storage adapter interface. Each implementation owns persistence for
|
|
5
5
|
* a single workspace's sessions and persons. Multi-tenancy is the host's
|
|
@@ -60,6 +60,36 @@ export interface StorageAdapter {
|
|
|
60
60
|
channel?: string;
|
|
61
61
|
scope?: "participant" | "viewable";
|
|
62
62
|
}): Promise<Session[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Upsert a name → person mapping. Names collapse case-insensitively
|
|
65
|
+
* (the row is keyed by `(person_id, lower(name))`).
|
|
66
|
+
*
|
|
67
|
+
* - `increment: true` (default): if the alias already exists, bump
|
|
68
|
+
* `usage_count` by 1 and replace `caller` / `last_used` with the
|
|
69
|
+
* input values.
|
|
70
|
+
* - `increment: false`: if the row already exists, do not touch it.
|
|
71
|
+
*
|
|
72
|
+
* Returns `null` if `personId` does not exist (engram_aliases FKs into
|
|
73
|
+
* engram_persons).
|
|
74
|
+
*/
|
|
75
|
+
upsertAlias(personId: string, input: {
|
|
76
|
+
name: string;
|
|
77
|
+
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
78
|
+
/** A person's aliases, ordered newest-used-first. */
|
|
79
|
+
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Upsert by `ref` (e.g. `slack:U12345`). Idempotent: writing the same
|
|
82
|
+
* ref multiple times converges. If the ref points to a different
|
|
83
|
+
* person than before, the row's `person_id` is updated — refs are
|
|
84
|
+
* the global handle, persons are reassignable underneath.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` if `input.person_id` does not exist.
|
|
87
|
+
*/
|
|
88
|
+
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
89
|
+
/** Lookup by ref. Returns null if unknown. */
|
|
90
|
+
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
91
|
+
/** All identities for a person, ordered newest-linked-first. */
|
|
92
|
+
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
63
93
|
}
|
|
64
94
|
/**
|
|
65
95
|
* Pure fold of an event log into the parts a Session needs. Used by adapters
|
package/openapi.json
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"name": "Persons",
|
|
23
23
|
"description": "person の作成・更新・検索"
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Identities",
|
|
27
|
+
"description": "外部 ID(slack: / email: など)の resolve と upsert"
|
|
28
|
+
},
|
|
25
29
|
{
|
|
26
30
|
"name": "Workspaces (admin)",
|
|
27
31
|
"description": "ワークスペースの管理(管理者トークン必須)"
|
|
@@ -798,6 +802,183 @@
|
|
|
798
802
|
]
|
|
799
803
|
}
|
|
800
804
|
},
|
|
805
|
+
"/v1/persons/{id}/aliases": {
|
|
806
|
+
"get": {
|
|
807
|
+
"summary": "この person の alias 一覧を取得する(直近使用が先頭)。",
|
|
808
|
+
"parameters": [
|
|
809
|
+
{
|
|
810
|
+
"name": "id",
|
|
811
|
+
"in": "path",
|
|
812
|
+
"required": true,
|
|
813
|
+
"schema": {
|
|
814
|
+
"type": "string"
|
|
815
|
+
},
|
|
816
|
+
"description": "person id。"
|
|
817
|
+
}
|
|
818
|
+
],
|
|
819
|
+
"responses": {
|
|
820
|
+
"200": {
|
|
821
|
+
"description": "alias 一覧"
|
|
822
|
+
},
|
|
823
|
+
"401": {
|
|
824
|
+
"description": "認証エラー"
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
"tags": [
|
|
828
|
+
"Persons"
|
|
829
|
+
]
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
"/v1/persons/{id}/aliases/{name}": {
|
|
833
|
+
"put": {
|
|
834
|
+
"summary": "person に alias を upsert する。name は case-insensitive で比較される。",
|
|
835
|
+
"parameters": [
|
|
836
|
+
{
|
|
837
|
+
"name": "id",
|
|
838
|
+
"in": "path",
|
|
839
|
+
"required": true,
|
|
840
|
+
"schema": {
|
|
841
|
+
"type": "string"
|
|
842
|
+
},
|
|
843
|
+
"description": "person id。"
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
"name": "name",
|
|
847
|
+
"in": "path",
|
|
848
|
+
"required": true,
|
|
849
|
+
"schema": {
|
|
850
|
+
"type": "string"
|
|
851
|
+
},
|
|
852
|
+
"description": "alias の名前(URL-encoded)。"
|
|
853
|
+
}
|
|
854
|
+
],
|
|
855
|
+
"requestBody": {
|
|
856
|
+
"required": true,
|
|
857
|
+
"content": {
|
|
858
|
+
"application/json": {
|
|
859
|
+
"schema": {
|
|
860
|
+
"$ref": "#/components/schemas/AliasUpsert"
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
"responses": {
|
|
866
|
+
"200": {
|
|
867
|
+
"description": "upsert された alias"
|
|
868
|
+
},
|
|
869
|
+
"400": {
|
|
870
|
+
"description": "リクエストボディが不正"
|
|
871
|
+
},
|
|
872
|
+
"401": {
|
|
873
|
+
"description": "認証エラー"
|
|
874
|
+
},
|
|
875
|
+
"404": {
|
|
876
|
+
"description": "person が見つからない"
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
"tags": [
|
|
880
|
+
"Persons"
|
|
881
|
+
]
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
"/v1/persons/{id}/identities": {
|
|
885
|
+
"get": {
|
|
886
|
+
"summary": "この person のすべての identity を取得する(直近 link が先頭)。",
|
|
887
|
+
"parameters": [
|
|
888
|
+
{
|
|
889
|
+
"name": "id",
|
|
890
|
+
"in": "path",
|
|
891
|
+
"required": true,
|
|
892
|
+
"schema": {
|
|
893
|
+
"type": "string"
|
|
894
|
+
},
|
|
895
|
+
"description": "person id。"
|
|
896
|
+
}
|
|
897
|
+
],
|
|
898
|
+
"responses": {
|
|
899
|
+
"200": {
|
|
900
|
+
"description": "identity 一覧"
|
|
901
|
+
},
|
|
902
|
+
"401": {
|
|
903
|
+
"description": "認証エラー"
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
"tags": [
|
|
907
|
+
"Persons"
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
"/v1/identities/{ref}": {
|
|
912
|
+
"put": {
|
|
913
|
+
"summary": "ref(例: `slack:U12345`、`email:foo@bar.com`)で identity を upsert する。",
|
|
914
|
+
"parameters": [
|
|
915
|
+
{
|
|
916
|
+
"name": "ref",
|
|
917
|
+
"in": "path",
|
|
918
|
+
"required": true,
|
|
919
|
+
"schema": {
|
|
920
|
+
"type": "string"
|
|
921
|
+
},
|
|
922
|
+
"description": "global identity ref(URL-encoded)。"
|
|
923
|
+
}
|
|
924
|
+
],
|
|
925
|
+
"requestBody": {
|
|
926
|
+
"required": true,
|
|
927
|
+
"content": {
|
|
928
|
+
"application/json": {
|
|
929
|
+
"schema": {
|
|
930
|
+
"$ref": "#/components/schemas/IdentityUpsert"
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
"responses": {
|
|
936
|
+
"200": {
|
|
937
|
+
"description": "upsert された identity"
|
|
938
|
+
},
|
|
939
|
+
"400": {
|
|
940
|
+
"description": "リクエストボディが不正"
|
|
941
|
+
},
|
|
942
|
+
"401": {
|
|
943
|
+
"description": "認証エラー"
|
|
944
|
+
},
|
|
945
|
+
"404": {
|
|
946
|
+
"description": "person_id が見つからない"
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
"tags": [
|
|
950
|
+
"Identities"
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
"get": {
|
|
954
|
+
"summary": "ref から identity を resolve する。",
|
|
955
|
+
"parameters": [
|
|
956
|
+
{
|
|
957
|
+
"name": "ref",
|
|
958
|
+
"in": "path",
|
|
959
|
+
"required": true,
|
|
960
|
+
"schema": {
|
|
961
|
+
"type": "string"
|
|
962
|
+
},
|
|
963
|
+
"description": "global identity ref(URL-encoded)。"
|
|
964
|
+
}
|
|
965
|
+
],
|
|
966
|
+
"responses": {
|
|
967
|
+
"200": {
|
|
968
|
+
"description": "identity 情報"
|
|
969
|
+
},
|
|
970
|
+
"401": {
|
|
971
|
+
"description": "認証エラー"
|
|
972
|
+
},
|
|
973
|
+
"404": {
|
|
974
|
+
"description": "ref が見つからない"
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
"tags": [
|
|
978
|
+
"Identities"
|
|
979
|
+
]
|
|
980
|
+
}
|
|
981
|
+
},
|
|
801
982
|
"/admin/v1/workspaces": {
|
|
802
983
|
"post": {
|
|
803
984
|
"summary": "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"engram",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@hexis-ai/engram-core": "^0.1.5",
|
|
53
|
-
"@hexis-ai/engram-sdk": "^0.
|
|
53
|
+
"@hexis-ai/engram-sdk": "^0.5.0",
|
|
54
54
|
"hono": "^4.6.0",
|
|
55
55
|
"zod": "^4.0.0"
|
|
56
56
|
},
|