@hexis-ai/engram-server 0.1.5 → 0.1.7

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.
@@ -3,7 +3,10 @@ import type { SqlClient } from "./postgres";
3
3
  export declare class PostgresKeyStore implements KeyStore {
4
4
  private readonly sql;
5
5
  constructor(sql: SqlClient);
6
- /** Create the key-store schema. Call once at boot. */
6
+ /**
7
+ * Apply all pending schema migrations. Safe to call repeatedly and
8
+ * concurrently with other instances — see `runMigrations` for details.
9
+ */
7
10
  ensureSchema(): Promise<void>;
8
11
  createWorkspace(input: {
9
12
  id?: string;
@@ -1,34 +1,16 @@
1
1
  import { generateRawKey, hashKey, isValidWorkspaceId, keyPrefix, } from "../key-store";
2
- const KEY_STORE_SCHEMA_SQL = `
3
- CREATE TABLE IF NOT EXISTS engram_workspaces (
4
- id TEXT PRIMARY KEY,
5
- name TEXT,
6
- metadata JSONB NOT NULL DEFAULT '{}',
7
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8
- );
9
-
10
- CREATE TABLE IF NOT EXISTS engram_api_keys (
11
- id TEXT PRIMARY KEY,
12
- workspace_id TEXT NOT NULL REFERENCES engram_workspaces(id) ON DELETE CASCADE,
13
- key_hash TEXT NOT NULL UNIQUE,
14
- prefix TEXT NOT NULL,
15
- name TEXT,
16
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
17
- last_used_at TIMESTAMPTZ,
18
- revoked_at TIMESTAMPTZ
19
- );
20
-
21
- CREATE INDEX IF NOT EXISTS idx_engram_api_keys_workspace
22
- ON engram_api_keys (workspace_id);
23
- `;
2
+ import { runMigrations } from "../migrator";
24
3
  export class PostgresKeyStore {
25
4
  sql;
26
5
  constructor(sql) {
27
6
  this.sql = sql;
28
7
  }
29
- /** Create the key-store schema. Call once at boot. */
8
+ /**
9
+ * Apply all pending schema migrations. Safe to call repeatedly and
10
+ * concurrently with other instances — see `runMigrations` for details.
11
+ */
30
12
  async ensureSchema() {
31
- await this.sql.unsafe(KEY_STORE_SCHEMA_SQL);
13
+ await runMigrations(this.sql);
32
14
  }
33
15
  async createWorkspace(input) {
34
16
  const id = input.id ?? crypto.randomUUID();
@@ -27,8 +27,8 @@ export declare class PostgresAdapter implements StorageAdapter {
27
27
  private readonly newPersonId;
28
28
  constructor(opts: PostgresAdapterOptions);
29
29
  /**
30
- * Create the schema. Call once at boot. Safe to invoke repeatedly.
31
- * Uses `sql.unsafe()` to ship the multi-statement DDL as a single batch.
30
+ * Apply all pending schema migrations. Safe to call repeatedly and
31
+ * concurrently with other instances see `runMigrations` for details.
32
32
  */
33
33
  ensureSchema(): Promise<void>;
34
34
  createSession(init: SessionInit & {
@@ -1,53 +1,5 @@
1
+ import { runMigrations } from "../migrator";
1
2
  import { foldEvents } from "../storage";
2
- const SCHEMA_SQL = `
3
- CREATE TABLE IF NOT EXISTS engram_sessions (
4
- workspace_id TEXT NOT NULL,
5
- id TEXT NOT NULL,
6
- title TEXT,
7
- channel TEXT,
8
- participants TEXT[] NOT NULL DEFAULT '{}',
9
- viewable_by TEXT[] NOT NULL DEFAULT '{}',
10
- created_at TIMESTAMPTZ NOT NULL,
11
- PRIMARY KEY (workspace_id, id)
12
- );
13
-
14
- -- Existing deployments may pre-date viewable_by; backfill the column on
15
- -- upgrade. Idempotent.
16
- ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS viewable_by TEXT[] NOT NULL DEFAULT '{}';
17
-
18
- CREATE TABLE IF NOT EXISTS engram_events (
19
- workspace_id TEXT NOT NULL,
20
- session_id TEXT NOT NULL,
21
- seq INTEGER NOT NULL,
22
- type TEXT NOT NULL,
23
- at TIMESTAMPTZ NOT NULL,
24
- payload JSONB NOT NULL,
25
- PRIMARY KEY (workspace_id, session_id, seq),
26
- FOREIGN KEY (workspace_id, session_id)
27
- REFERENCES engram_sessions(workspace_id, id) ON DELETE CASCADE
28
- );
29
-
30
- CREATE TABLE IF NOT EXISTS engram_persons (
31
- workspace_id TEXT NOT NULL,
32
- id TEXT NOT NULL,
33
- display_name TEXT,
34
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
35
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
36
- PRIMARY KEY (workspace_id, id)
37
- );
38
-
39
- CREATE INDEX IF NOT EXISTS idx_engram_sessions_workspace_created
40
- ON engram_sessions (workspace_id, created_at DESC);
41
-
42
- -- Person-axis lookups. GIN supports the @> contains operator efficiently.
43
- CREATE INDEX IF NOT EXISTS idx_engram_sessions_participants
44
- ON engram_sessions USING GIN (participants);
45
- CREATE INDEX IF NOT EXISTS idx_engram_sessions_viewable_by
46
- ON engram_sessions USING GIN (viewable_by);
47
-
48
- CREATE INDEX IF NOT EXISTS idx_engram_persons_updated
49
- ON engram_persons (workspace_id, updated_at DESC);
50
- `;
51
3
  /**
52
4
  * 10-char alphanumeric id, e.g. `p_a8b3c2d4`. Cryptographic randomness via
53
5
  * the platform's getRandomValues; collision probability negligible for any
@@ -72,11 +24,11 @@ export class PostgresAdapter {
72
24
  this.newPersonId = opts.newPersonId ?? defaultPersonId;
73
25
  }
74
26
  /**
75
- * Create the schema. Call once at boot. Safe to invoke repeatedly.
76
- * Uses `sql.unsafe()` to ship the multi-statement DDL as a single batch.
27
+ * Apply all pending schema migrations. Safe to call repeatedly and
28
+ * concurrently with other instances see `runMigrations` for details.
77
29
  */
78
30
  async ensureSchema() {
79
- await this.sql.unsafe(SCHEMA_SQL);
31
+ await runMigrations(this.sql);
80
32
  }
81
33
  // --- Sessions -----------------------------------------------------
82
34
  async createSession(init) {
package/dist/main.js CHANGED
@@ -11,13 +11,10 @@
11
11
  * DATABASE_URL if unset, falls back to InMemoryKeyStore +
12
12
  * InMemoryAdapter (NOT durable across restarts)
13
13
  * DATABASE_SOCKET_PATH Cloud SQL Auth Proxy unix socket dir
14
- * ENGRAM_API_KEY legacy single-tenant key. When set, a workspace
15
- * identified by ENGRAM_WORKSPACE_ID is created on
16
- * boot and this raw key is registered against it,
17
- * keeping existing single-tenant deploys working
18
- * while the multi-tenant flow rolls out.
19
- * ENGRAM_WORKSPACE_ID default "default" — the workspace id used for
20
- * the legacy bootstrap above.
14
+ *
15
+ * Workspaces and their API keys are provisioned exclusively through the
16
+ * admin API. There is no single-tenant fallback — every caller must hold a
17
+ * workspace-scoped key issued by `POST /admin/v1/workspaces`.
21
18
  */
22
19
  import { createServer } from "./server";
23
20
  import { InMemoryAdapter } from "./adapters/memory";
@@ -26,8 +23,6 @@ import { InMemoryKeyStore } from "./adapters/memory-key-store";
26
23
  import { PostgresKeyStore } from "./adapters/postgres-key-store";
27
24
  const PORT = Number(process.env.PORT ?? 8080);
28
25
  const ADMIN_TOKEN = process.env.ENGRAM_ADMIN_TOKEN;
29
- const LEGACY_API_KEY = process.env.ENGRAM_API_KEY;
30
- const LEGACY_WORKSPACE_ID = process.env.ENGRAM_WORKSPACE_ID ?? "default";
31
26
  const DATABASE_URL = process.env.DATABASE_URL;
32
27
  const DATABASE_SOCKET_PATH = process.env.DATABASE_SOCKET_PATH;
33
28
  if (!ADMIN_TOKEN) {
@@ -35,11 +30,6 @@ if (!ADMIN_TOKEN) {
35
30
  process.exit(1);
36
31
  }
37
32
  const { keyStore, getStorage } = await buildStores();
38
- if (LEGACY_API_KEY) {
39
- await keyStore.createWorkspace({ id: LEGACY_WORKSPACE_ID, name: "Legacy" });
40
- await keyStore.registerLegacyKey(LEGACY_WORKSPACE_ID, LEGACY_API_KEY, "ENGRAM_API_KEY");
41
- console.log(`[engram-server] legacy key bootstrapped (workspace=${LEGACY_WORKSPACE_ID})`);
42
- }
43
33
  const app = createServer({
44
34
  auth: async (key) => {
45
35
  const r = await keyStore.resolveKey(key);
@@ -0,0 +1,2 @@
1
+ export declare const name = "0001-baseline";
2
+ export declare const sql = "\n-- Session storage (workspace-scoped).\nCREATE TABLE IF NOT EXISTS engram_sessions (\n workspace_id TEXT NOT NULL,\n id TEXT NOT NULL,\n title TEXT,\n channel TEXT,\n participants TEXT[] NOT NULL DEFAULT '{}',\n viewable_by TEXT[] NOT NULL DEFAULT '{}',\n created_at TIMESTAMPTZ NOT NULL,\n PRIMARY KEY (workspace_id, id)\n);\n\n-- Deployments that pre-date viewable_by need the column backfilled.\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS viewable_by TEXT[] NOT NULL DEFAULT '{}';\n\nCREATE TABLE IF NOT EXISTS engram_events (\n workspace_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n type TEXT NOT NULL,\n at TIMESTAMPTZ NOT NULL,\n payload JSONB NOT NULL,\n PRIMARY KEY (workspace_id, session_id, seq),\n FOREIGN KEY (workspace_id, session_id)\n REFERENCES engram_sessions(workspace_id, id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS engram_persons (\n workspace_id TEXT NOT NULL,\n id TEXT NOT NULL,\n display_name TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (workspace_id, id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_engram_sessions_workspace_created\n ON engram_sessions (workspace_id, created_at DESC);\n\n-- Person-axis lookups. GIN supports the @> contains operator efficiently.\nCREATE INDEX IF NOT EXISTS idx_engram_sessions_participants\n ON engram_sessions USING GIN (participants);\nCREATE INDEX IF NOT EXISTS idx_engram_sessions_viewable_by\n ON engram_sessions USING GIN (viewable_by);\n\nCREATE INDEX IF NOT EXISTS idx_engram_persons_updated\n ON engram_persons (workspace_id, updated_at DESC);\n\n-- Control plane: workspaces + API keys.\nCREATE TABLE IF NOT EXISTS engram_workspaces (\n id TEXT PRIMARY KEY,\n name TEXT,\n metadata JSONB NOT NULL DEFAULT '{}',\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE IF NOT EXISTS engram_api_keys (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL REFERENCES engram_workspaces(id) ON DELETE CASCADE,\n key_hash TEXT NOT NULL UNIQUE,\n prefix TEXT NOT NULL,\n name TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n last_used_at TIMESTAMPTZ,\n revoked_at TIMESTAMPTZ\n);\n\nCREATE INDEX IF NOT EXISTS idx_engram_api_keys_workspace\n ON engram_api_keys (workspace_id);\n";
@@ -0,0 +1,72 @@
1
+ export const name = "0001-baseline";
2
+ export const sql = `
3
+ -- Session storage (workspace-scoped).
4
+ CREATE TABLE IF NOT EXISTS engram_sessions (
5
+ workspace_id TEXT NOT NULL,
6
+ id TEXT NOT NULL,
7
+ title TEXT,
8
+ channel TEXT,
9
+ participants TEXT[] NOT NULL DEFAULT '{}',
10
+ viewable_by TEXT[] NOT NULL DEFAULT '{}',
11
+ created_at TIMESTAMPTZ NOT NULL,
12
+ PRIMARY KEY (workspace_id, id)
13
+ );
14
+
15
+ -- Deployments that pre-date viewable_by need the column backfilled.
16
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS viewable_by TEXT[] NOT NULL DEFAULT '{}';
17
+
18
+ CREATE TABLE IF NOT EXISTS engram_events (
19
+ workspace_id TEXT NOT NULL,
20
+ session_id TEXT NOT NULL,
21
+ seq INTEGER NOT NULL,
22
+ type TEXT NOT NULL,
23
+ at TIMESTAMPTZ NOT NULL,
24
+ payload JSONB NOT NULL,
25
+ PRIMARY KEY (workspace_id, session_id, seq),
26
+ FOREIGN KEY (workspace_id, session_id)
27
+ REFERENCES engram_sessions(workspace_id, id) ON DELETE CASCADE
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS engram_persons (
31
+ workspace_id TEXT NOT NULL,
32
+ id TEXT NOT NULL,
33
+ display_name TEXT,
34
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
35
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
36
+ PRIMARY KEY (workspace_id, id)
37
+ );
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_engram_sessions_workspace_created
40
+ ON engram_sessions (workspace_id, created_at DESC);
41
+
42
+ -- Person-axis lookups. GIN supports the @> contains operator efficiently.
43
+ CREATE INDEX IF NOT EXISTS idx_engram_sessions_participants
44
+ ON engram_sessions USING GIN (participants);
45
+ CREATE INDEX IF NOT EXISTS idx_engram_sessions_viewable_by
46
+ ON engram_sessions USING GIN (viewable_by);
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_engram_persons_updated
49
+ ON engram_persons (workspace_id, updated_at DESC);
50
+
51
+ -- Control plane: workspaces + API keys.
52
+ CREATE TABLE IF NOT EXISTS engram_workspaces (
53
+ id TEXT PRIMARY KEY,
54
+ name TEXT,
55
+ metadata JSONB NOT NULL DEFAULT '{}',
56
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS engram_api_keys (
60
+ id TEXT PRIMARY KEY,
61
+ workspace_id TEXT NOT NULL REFERENCES engram_workspaces(id) ON DELETE CASCADE,
62
+ key_hash TEXT NOT NULL UNIQUE,
63
+ prefix TEXT NOT NULL,
64
+ name TEXT,
65
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66
+ last_used_at TIMESTAMPTZ,
67
+ revoked_at TIMESTAMPTZ
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_engram_api_keys_workspace
71
+ ON engram_api_keys (workspace_id);
72
+ `;
@@ -0,0 +1,12 @@
1
+ export interface Migration {
2
+ name: string;
3
+ sql: string;
4
+ }
5
+ /**
6
+ * Schema migrations, applied in array order. Add a new file under
7
+ * `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
8
+ * here. Each migration's SQL must be idempotent (CREATE TABLE IF NOT
9
+ * EXISTS, ADD COLUMN IF NOT EXISTS, etc.) so a first apply on a DB that
10
+ * predates the migrator is a no-op.
11
+ */
12
+ export declare const MIGRATIONS: Migration[];
@@ -0,0 +1,9 @@
1
+ import * as m0001 from "./0001-baseline";
2
+ /**
3
+ * Schema migrations, applied in array order. Add a new file under
4
+ * `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
5
+ * here. Each migration's SQL must be idempotent (CREATE TABLE IF NOT
6
+ * EXISTS, ADD COLUMN IF NOT EXISTS, etc.) so a first apply on a DB that
7
+ * predates the migrator is a no-op.
8
+ */
9
+ export const MIGRATIONS = [{ name: m0001.name, sql: m0001.sql }];
@@ -0,0 +1,17 @@
1
+ import type { SqlClient } from "./adapters/postgres";
2
+ import { type Migration } from "./migrations";
3
+ /**
4
+ * Apply pending schema migrations in order. Records applied migrations in
5
+ * `engram_schema_migrations` so subsequent boots skip them.
6
+ *
7
+ * Each migration's SQL must itself be idempotent (IF NOT EXISTS, etc.) —
8
+ * the tracking table protects against unnecessary re-runs but a partial
9
+ * failure between executing the SQL and inserting the tracking row would
10
+ * cause a replay on next boot, and we want that replay to be a no-op.
11
+ *
12
+ * Safe to call concurrently from multiple instances: the INSERT uses ON
13
+ * CONFLICT and the migration SQL itself is idempotent, so two boots
14
+ * applying the same migration in parallel just race to a consistent end
15
+ * state.
16
+ */
17
+ export declare function runMigrations(sql: SqlClient, migrations?: readonly Migration[]): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { MIGRATIONS } from "./migrations";
2
+ /**
3
+ * Apply pending schema migrations in order. Records applied migrations in
4
+ * `engram_schema_migrations` so subsequent boots skip them.
5
+ *
6
+ * Each migration's SQL must itself be idempotent (IF NOT EXISTS, etc.) —
7
+ * the tracking table protects against unnecessary re-runs but a partial
8
+ * failure between executing the SQL and inserting the tracking row would
9
+ * cause a replay on next boot, and we want that replay to be a no-op.
10
+ *
11
+ * Safe to call concurrently from multiple instances: the INSERT uses ON
12
+ * CONFLICT and the migration SQL itself is idempotent, so two boots
13
+ * applying the same migration in parallel just race to a consistent end
14
+ * state.
15
+ */
16
+ export async function runMigrations(sql, migrations = MIGRATIONS) {
17
+ await sql.unsafe(`
18
+ CREATE TABLE IF NOT EXISTS engram_schema_migrations (
19
+ name TEXT PRIMARY KEY,
20
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
21
+ )
22
+ `);
23
+ const appliedRows = await sql `
24
+ SELECT name FROM engram_schema_migrations
25
+ `;
26
+ const applied = new Set(appliedRows.map((r) => r.name));
27
+ for (const m of migrations) {
28
+ if (applied.has(m.name))
29
+ continue;
30
+ await sql.unsafe(m.sql);
31
+ await sql `
32
+ INSERT INTO engram_schema_migrations (name)
33
+ VALUES (${m.name})
34
+ ON CONFLICT (name) DO NOTHING
35
+ `;
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@hexis-ai/engram-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
5
- "keywords": ["engram", "agents", "search", "hono", "postgres", "server"],
5
+ "keywords": [
6
+ "engram",
7
+ "agents",
8
+ "search",
9
+ "hono",
10
+ "postgres",
11
+ "server"
12
+ ],
6
13
  "homepage": "https://github.com/hexis-ltd/engram#readme",
7
14
  "repository": {
8
15
  "type": "git",
@@ -42,14 +49,16 @@
42
49
  },
43
50
  "dependencies": {
44
51
  "@hexis-ai/engram-core": "^0.1.5",
45
- "@hexis-ai/engram-sdk": "^0.1.5",
52
+ "@hexis-ai/engram-sdk": "^0.2.0",
46
53
  "hono": "^4.6.0"
47
54
  },
48
55
  "peerDependencies": {
49
56
  "postgres": "^3.4.0"
50
57
  },
51
58
  "peerDependenciesMeta": {
52
- "postgres": { "optional": true }
59
+ "postgres": {
60
+ "optional": true
61
+ }
53
62
  },
54
63
  "devDependencies": {
55
64
  "postgres": "^3.4.0"