@hexis-ai/engram-server 0.3.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.
@@ -1,4 +1,4 @@
1
- import { generateRawKey, hashKey, isValidWorkspaceId, keyPrefix, } from "../key-store";
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.id ?? crypto.randomUUID();
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 = generateRawKey();
41
+ const { id, hash, prefix, raw } = mintKey();
44
42
  const row = {
45
- id: crypto.randomUUID(),
43
+ id,
46
44
  workspaceId,
47
- keyHash: hashKey(raw),
48
- prefix: keyPrefix(raw),
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 = hashKey(rawKey);
81
+ const { id, hash, prefix } = deriveKey(rawKey);
84
82
  if (this.byHash.has(hash))
85
83
  return;
86
84
  const row = {
87
- id: crypto.randomUUID(),
85
+ id,
88
86
  workspaceId,
89
87
  keyHash: hash,
90
- prefix: keyPrefix(rawKey),
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;
@@ -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
- display_name: input.display_name !== undefined
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 { generateRawKey, hashKey, isValidWorkspaceId, keyPrefix, } from "../key-store";
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.id ?? crypto.randomUUID();
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 = generateRawKey();
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 = hashKey(rawKey);
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}, ${keyPrefix(rawKey)}, ${name ?? null})
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 `
@@ -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
+ }