@hexis-ai/engram-server 0.11.3 → 0.13.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.
Files changed (42) hide show
  1. package/dist/adapters/memory-key-store.d.ts +4 -0
  2. package/dist/adapters/memory-key-store.js +12 -0
  3. package/dist/adapters/memory.js +47 -66
  4. package/dist/adapters/pg-tagged.d.ts +18 -0
  5. package/dist/adapters/pg-tagged.js +29 -0
  6. package/dist/adapters/postgres-key-store.d.ts +4 -0
  7. package/dist/adapters/postgres-key-store.js +14 -3
  8. package/dist/adapters/postgres-org-store.d.ts +42 -0
  9. package/dist/adapters/postgres-org-store.js +120 -0
  10. package/dist/adapters/postgres.js +57 -80
  11. package/dist/adapters/util.d.ts +27 -0
  12. package/dist/adapters/util.js +47 -0
  13. package/dist/admin.d.ts +26 -4
  14. package/dist/admin.js +126 -7
  15. package/dist/auth-resolver.d.ts +32 -0
  16. package/dist/auth-resolver.js +53 -0
  17. package/dist/auth.d.ts +196 -0
  18. package/dist/auth.js +164 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.js +4 -0
  21. package/dist/key-store.d.ts +5 -0
  22. package/dist/main.js +84 -26
  23. package/dist/migrations/0006-auth.d.ts +2 -0
  24. package/dist/migrations/0006-auth.js +84 -0
  25. package/dist/migrations/0007-orgs.d.ts +2 -0
  26. package/dist/migrations/0007-orgs.js +59 -0
  27. package/dist/migrations/index.js +4 -0
  28. package/dist/openapi.js +340 -3
  29. package/dist/org-store.d.ts +73 -0
  30. package/dist/org-store.js +12 -0
  31. package/dist/routes/orgs.d.ts +27 -0
  32. package/dist/routes/orgs.js +185 -0
  33. package/dist/schemas.d.ts +18 -0
  34. package/dist/schemas.js +19 -0
  35. package/dist/server.d.ts +39 -0
  36. package/dist/server.js +85 -7
  37. package/dist/services/orgs.d.ts +95 -0
  38. package/dist/services/orgs.js +159 -0
  39. package/dist/storage.d.ts +6 -0
  40. package/dist/storage.js +14 -0
  41. package/openapi.json +1279 -1
  42. package/package.json +5 -11
package/dist/auth.d.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * better-auth instance for engram-server.
3
+ *
4
+ * Two auth modes coexist on /v1/*:
5
+ * - x-api-key / Bearer → machine callers (monet telemetry / MCP),
6
+ * resolved through KeyStore → workspace.
7
+ * - cookie session → human users from engram-web,
8
+ * resolved through this module → user →
9
+ * membership check → workspace.
10
+ *
11
+ * Tables live under \`engram_auth_*\` (see migration 0006-auth) so the
12
+ * shorter \`engram_session\` namespace remains agent-trajectory only.
13
+ *
14
+ * Required env (production):
15
+ * ENGRAM_AUTH_SECRET cookie/JWT signing secret (>= 32 chars)
16
+ * ENGRAM_AUTH_URL full base URL of this server
17
+ * (e.g. https://api.engram.hexis.ltd)
18
+ * ENGRAM_AUTH_GOOGLE_ID Google OAuth client id
19
+ * ENGRAM_AUTH_GOOGLE_SECRET Google OAuth client secret
20
+ *
21
+ * Optional:
22
+ * ENGRAM_AUTH_COOKIE_DOMAIN set to .hexis.ltd to share cookies
23
+ * between engram.hexis.ltd (web) and
24
+ * api.engram.hexis.ltd (this server)
25
+ * ENGRAM_AUTH_TRUSTED_ORIGINS comma-separated extra origins, e.g.
26
+ * engram-web's URL in dev
27
+ * ENGRAM_AUTH_EMAIL_DOMAIN restrict sign-in to one email domain
28
+ * (default: hexis.ltd)
29
+ */
30
+ import type { Pool } from "pg";
31
+ export interface BuildAuthOptions {
32
+ /** node-postgres Pool for better-auth's Kysely adapter. */
33
+ pool: Pool;
34
+ /** Cookie/JWT signing secret. Required, >= 32 chars in prod. */
35
+ secret: string;
36
+ /** Full base URL of this server (e.g. https://api.engram.hexis.ltd). */
37
+ baseURL: string;
38
+ google: {
39
+ clientId: string;
40
+ clientSecret: string;
41
+ };
42
+ /** Cookie scope when web + api are on sibling subdomains. */
43
+ cookieDomain?: string;
44
+ /** Additional trustedOrigins for CSRF / redirect allowlist. */
45
+ trustedOrigins?: string[];
46
+ /** Restrict sign-in to a single email domain. Default: hexis.ltd. */
47
+ emailDomain?: string;
48
+ /** Use Secure cookies (HTTPS only). Defaults to NODE_ENV=production. */
49
+ secureCookies?: boolean;
50
+ }
51
+ export type EngramAuth = ReturnType<typeof buildAuth>;
52
+ /**
53
+ * Construct the better-auth instance. The returned value exposes
54
+ * - \`handler(req: Request): Response\` for Hono \`.all("/auth/*", ...)\`
55
+ * - \`api.getSession({ headers })\` for cookie → session lookup
56
+ */
57
+ export declare function buildAuth(opts: BuildAuthOptions): import("better-auth").Auth<{
58
+ database: Pool;
59
+ secret: string;
60
+ baseURL: string;
61
+ basePath: string;
62
+ trustedOrigins: string[];
63
+ user: {
64
+ modelName: "engram_auth_users";
65
+ fields: {
66
+ emailVerified: string;
67
+ createdAt: string;
68
+ updatedAt: string;
69
+ };
70
+ };
71
+ session: {
72
+ modelName: "engram_auth_sessions";
73
+ fields: {
74
+ userId: string;
75
+ expiresAt: string;
76
+ ipAddress: string;
77
+ userAgent: string;
78
+ createdAt: string;
79
+ updatedAt: string;
80
+ token: string;
81
+ };
82
+ additionalFields: {
83
+ currentWorkspaceId: {
84
+ type: "string";
85
+ required: false;
86
+ input: false;
87
+ fieldName: string;
88
+ };
89
+ };
90
+ cookieCache: {
91
+ enabled: true;
92
+ maxAge: number;
93
+ };
94
+ };
95
+ account: {
96
+ modelName: "engram_auth_accounts";
97
+ fields: {
98
+ userId: string;
99
+ accountId: string;
100
+ providerId: string;
101
+ accessToken: string;
102
+ refreshToken: string;
103
+ idToken: string;
104
+ accessTokenExpiresAt: string;
105
+ refreshTokenExpiresAt: string;
106
+ scope: string;
107
+ password: string;
108
+ createdAt: string;
109
+ updatedAt: string;
110
+ };
111
+ accountLinking: {
112
+ enabled: true;
113
+ trustedProviders: "google"[];
114
+ };
115
+ };
116
+ verification: {
117
+ modelName: "engram_auth_verifications";
118
+ fields: {
119
+ expiresAt: string;
120
+ createdAt: string;
121
+ updatedAt: string;
122
+ };
123
+ };
124
+ advanced: {
125
+ database: {
126
+ generateId: () => string;
127
+ };
128
+ cookiePrefix: string;
129
+ cookies: {
130
+ sessionToken: {
131
+ attributes: {
132
+ domain: string;
133
+ sameSite: "lax";
134
+ secure: boolean;
135
+ httpOnly: true;
136
+ path: string;
137
+ };
138
+ };
139
+ } | undefined;
140
+ useSecureCookies: boolean;
141
+ crossSubDomainCookies: {
142
+ enabled: true;
143
+ domain: string;
144
+ } | undefined;
145
+ };
146
+ socialProviders: {
147
+ google: {
148
+ clientId: string;
149
+ clientSecret: string;
150
+ };
151
+ };
152
+ databaseHooks: {
153
+ user: {
154
+ create: {
155
+ before: (user: {
156
+ id: string;
157
+ createdAt: Date;
158
+ updatedAt: Date;
159
+ email: string;
160
+ emailVerified: boolean;
161
+ name: string;
162
+ image?: string | null | undefined;
163
+ } & Record<string, unknown>) => Promise<{
164
+ data: {
165
+ id: string;
166
+ createdAt: Date;
167
+ updatedAt: Date;
168
+ email: string;
169
+ emailVerified: boolean;
170
+ name: string;
171
+ image?: string | null | undefined;
172
+ } & Record<string, unknown>;
173
+ }>;
174
+ };
175
+ };
176
+ };
177
+ plugins: [{
178
+ id: "bearer";
179
+ version: string;
180
+ hooks: {
181
+ before: {
182
+ matcher(context: import("better-auth").HookEndpointContext): boolean;
183
+ handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
184
+ context: {
185
+ headers: Headers;
186
+ };
187
+ } | undefined>;
188
+ }[];
189
+ after: {
190
+ matcher(context: import("better-auth").HookEndpointContext): true;
191
+ handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
192
+ }[];
193
+ };
194
+ options: import("better-auth/plugins").BearerOptions | undefined;
195
+ }];
196
+ }>;
package/dist/auth.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * better-auth instance for engram-server.
3
+ *
4
+ * Two auth modes coexist on /v1/*:
5
+ * - x-api-key / Bearer → machine callers (monet telemetry / MCP),
6
+ * resolved through KeyStore → workspace.
7
+ * - cookie session → human users from engram-web,
8
+ * resolved through this module → user →
9
+ * membership check → workspace.
10
+ *
11
+ * Tables live under \`engram_auth_*\` (see migration 0006-auth) so the
12
+ * shorter \`engram_session\` namespace remains agent-trajectory only.
13
+ *
14
+ * Required env (production):
15
+ * ENGRAM_AUTH_SECRET cookie/JWT signing secret (>= 32 chars)
16
+ * ENGRAM_AUTH_URL full base URL of this server
17
+ * (e.g. https://api.engram.hexis.ltd)
18
+ * ENGRAM_AUTH_GOOGLE_ID Google OAuth client id
19
+ * ENGRAM_AUTH_GOOGLE_SECRET Google OAuth client secret
20
+ *
21
+ * Optional:
22
+ * ENGRAM_AUTH_COOKIE_DOMAIN set to .hexis.ltd to share cookies
23
+ * between engram.hexis.ltd (web) and
24
+ * api.engram.hexis.ltd (this server)
25
+ * ENGRAM_AUTH_TRUSTED_ORIGINS comma-separated extra origins, e.g.
26
+ * engram-web's URL in dev
27
+ * ENGRAM_AUTH_EMAIL_DOMAIN restrict sign-in to one email domain
28
+ * (default: hexis.ltd)
29
+ */
30
+ import { randomUUID } from "node:crypto";
31
+ import { betterAuth } from "better-auth";
32
+ import { bearer } from "better-auth/plugins";
33
+ /**
34
+ * Construct the better-auth instance. The returned value exposes
35
+ * - \`handler(req: Request): Response\` for Hono \`.all("/auth/*", ...)\`
36
+ * - \`api.getSession({ headers })\` for cookie → session lookup
37
+ */
38
+ export function buildAuth(opts) {
39
+ const emailDomain = opts.emailDomain ?? "hexis.ltd";
40
+ const secure = opts.secureCookies ?? process.env.NODE_ENV === "production";
41
+ return betterAuth({
42
+ database: opts.pool,
43
+ secret: opts.secret,
44
+ baseURL: opts.baseURL,
45
+ basePath: "/auth",
46
+ trustedOrigins: opts.trustedOrigins ?? [],
47
+ // ---------- table mapping → engram_auth_* ---------------------
48
+ user: {
49
+ modelName: "engram_auth_users",
50
+ fields: {
51
+ emailVerified: "email_verified",
52
+ createdAt: "created_at",
53
+ updatedAt: "updated_at",
54
+ },
55
+ },
56
+ session: {
57
+ modelName: "engram_auth_sessions",
58
+ fields: {
59
+ userId: "user_id",
60
+ expiresAt: "expires_at",
61
+ ipAddress: "ip_address",
62
+ userAgent: "user_agent",
63
+ createdAt: "created_at",
64
+ updatedAt: "updated_at",
65
+ token: "token",
66
+ },
67
+ additionalFields: {
68
+ currentWorkspaceId: {
69
+ type: "string",
70
+ required: false,
71
+ input: false,
72
+ fieldName: "current_workspace_id",
73
+ },
74
+ },
75
+ cookieCache: { enabled: true, maxAge: 5 * 60 },
76
+ },
77
+ account: {
78
+ modelName: "engram_auth_accounts",
79
+ fields: {
80
+ userId: "user_id",
81
+ accountId: "account_id",
82
+ providerId: "provider_id",
83
+ accessToken: "access_token",
84
+ refreshToken: "refresh_token",
85
+ idToken: "id_token",
86
+ accessTokenExpiresAt: "access_token_expires_at",
87
+ refreshTokenExpiresAt: "refresh_token_expires_at",
88
+ scope: "scope",
89
+ password: "password",
90
+ createdAt: "created_at",
91
+ updatedAt: "updated_at",
92
+ },
93
+ // Auto-link the Google account to a same-email user. Engram
94
+ // doesn't pre-seed users — every user shows up via OAuth — so
95
+ // this is mostly defensive (covers DB restores / re-imports).
96
+ accountLinking: { enabled: true, trustedProviders: ["google"] },
97
+ },
98
+ verification: {
99
+ modelName: "engram_auth_verifications",
100
+ fields: {
101
+ expiresAt: "expires_at",
102
+ createdAt: "created_at",
103
+ updatedAt: "updated_at",
104
+ },
105
+ },
106
+ // ---------- ID + cookie scope ---------------------------------
107
+ advanced: {
108
+ database: {
109
+ generateId: () => `u_${randomUUID().replace(/-/g, "").substring(0, 16)}`,
110
+ },
111
+ // Distinct prefix so engram's cookie doesn't clobber monet's
112
+ // (both live under .hexis.ltd, both use better-auth defaults).
113
+ cookiePrefix: "engram",
114
+ cookies: opts.cookieDomain
115
+ ? {
116
+ sessionToken: {
117
+ attributes: {
118
+ domain: opts.cookieDomain,
119
+ sameSite: "lax",
120
+ secure,
121
+ httpOnly: true,
122
+ path: "/",
123
+ },
124
+ },
125
+ }
126
+ : undefined,
127
+ useSecureCookies: secure,
128
+ crossSubDomainCookies: opts.cookieDomain
129
+ ? { enabled: true, domain: opts.cookieDomain }
130
+ : undefined,
131
+ },
132
+ // ---------- providers -----------------------------------------
133
+ socialProviders: {
134
+ google: {
135
+ clientId: opts.google.clientId,
136
+ clientSecret: opts.google.clientSecret,
137
+ },
138
+ },
139
+ // ---------- email-domain allowlist ----------------------------
140
+ // Reject sign-ins whose verified email is outside the allowlist.
141
+ // Throwing here surfaces as a generic OAuth failure to the
142
+ // client; we deliberately don't expose the rejected domain.
143
+ databaseHooks: {
144
+ user: {
145
+ create: {
146
+ before: async (user) => {
147
+ const email = user.email ?? "";
148
+ const domain = email.split("@")[1]?.toLowerCase() ?? "";
149
+ if (domain !== emailDomain) {
150
+ throw new Error(`email_domain_not_allowed:${domain}`);
151
+ }
152
+ return { data: user };
153
+ },
154
+ },
155
+ },
156
+ },
157
+ // ---------- plugins -------------------------------------------
158
+ plugins: [
159
+ // Accept Authorization: Bearer <session-token> for clients
160
+ // that can't carry cookies (SPA → cross-domain XHR fallback).
161
+ bearer(),
162
+ ],
163
+ });
164
+ }
package/dist/index.d.ts CHANGED
@@ -8,3 +8,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
10
  export { buildOpenApiDocument } from "./openapi";
11
+ export { buildAuth, type BuildAuthOptions, type EngramAuth } from "./auth";
12
+ export { makeCookieAuthResolver, type CookieAuthResolver, type CookieResolverOptions, } from "./auth-resolver";
13
+ export { type OrgStore, type OrgRow, type OrgMembershipRow, } from "./org-store";
14
+ export { PostgresOrgStore } from "./adapters/postgres-org-store";
package/dist/index.js CHANGED
@@ -8,3 +8,7 @@ export { InMemoryKeyStore } from "./adapters/memory-key-store";
8
8
  export { PostgresKeyStore } from "./adapters/postgres-key-store";
9
9
  export { createAdminRouter } from "./admin";
10
10
  export { buildOpenApiDocument } from "./openapi";
11
+ export { buildAuth } from "./auth";
12
+ export { makeCookieAuthResolver, } from "./auth-resolver";
13
+ export {} from "./org-store";
14
+ export { PostgresOrgStore } from "./adapters/postgres-org-store";
@@ -36,6 +36,11 @@ export interface KeyStore {
36
36
  }): Promise<Workspace>;
37
37
  getWorkspace(id: string): Promise<Workspace | null>;
38
38
  listWorkspaces(): Promise<Workspace[]>;
39
+ /** Patch a workspace's name (and/or metadata). Returns the updated row. */
40
+ updateWorkspace(id: string, patch: {
41
+ name?: string;
42
+ metadata?: Record<string, unknown>;
43
+ }): Promise<Workspace>;
39
44
  /** Hard delete: cascades to keys, sessions, and events for this workspace. */
40
45
  deleteWorkspace(id: string): Promise<void>;
41
46
  issueKey(workspaceId: string, opts?: {
package/dist/main.js CHANGED
@@ -1,26 +1,12 @@
1
- /**
2
- * Production entrypoint.
3
- *
4
- * Required env:
5
- * ENGRAM_ADMIN_TOKEN platform-level bearer for `/admin/v1/*`. Treat as a
6
- * root credential — anyone with it can mint workspaces
7
- * and API keys.
8
- *
9
- * Optional env:
10
- * PORT default 8080
11
- * DATABASE_URL if unset, falls back to InMemoryKeyStore +
12
- * InMemoryAdapter (NOT durable across restarts)
13
- * DATABASE_SOCKET_PATH Cloud SQL Auth Proxy unix socket dir
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`.
18
- */
19
1
  import { createServer } from "./server";
20
2
  import { InMemoryAdapter } from "./adapters/memory";
21
3
  import { PostgresAdapter } from "./adapters/postgres";
22
4
  import { InMemoryKeyStore } from "./adapters/memory-key-store";
23
5
  import { PostgresKeyStore } from "./adapters/postgres-key-store";
6
+ import { pgSqlClient } from "./adapters/pg-tagged";
7
+ import { buildAuth } from "./auth";
8
+ import { makeCookieAuthResolver } from "./auth-resolver";
9
+ import { PostgresOrgStore } from "./adapters/postgres-org-store";
24
10
  const PORT = Number(process.env.PORT ?? 8080);
25
11
  const ADMIN_TOKEN = process.env.ENGRAM_ADMIN_TOKEN;
26
12
  const DATABASE_URL = process.env.DATABASE_URL;
@@ -29,7 +15,13 @@ if (!ADMIN_TOKEN) {
29
15
  console.error("[engram-server] ENGRAM_ADMIN_TOKEN is required");
30
16
  process.exit(1);
31
17
  }
32
- const { keyStore, getStorage } = await buildStores();
18
+ const pool = await buildPool();
19
+ const { keyStore, getStorage } = await buildStores(pool);
20
+ const { authHandler, cookieAuth, orgStore } = await buildCookieAuth(pool, getStorage);
21
+ const CORS_ORIGINS = (process.env.ENGRAM_CORS_ORIGINS ?? "")
22
+ .split(",")
23
+ .map((s) => s.trim())
24
+ .filter(Boolean);
33
25
  const app = createServer({
34
26
  auth: async (key) => {
35
27
  const r = await keyStore.resolveKey(key);
@@ -37,12 +29,39 @@ const app = createServer({
37
29
  return null;
38
30
  return { workspaceId: r.workspaceId, storage: getStorage(r.workspaceId) };
39
31
  },
40
- admin: { token: ADMIN_TOKEN, keyStore },
32
+ ...(authHandler ? { authHandler } : {}),
33
+ ...(cookieAuth ? { cookieAuth } : {}),
34
+ ...(orgStore ? { orgStore } : {}),
35
+ keyStore,
36
+ ...(CORS_ORIGINS.length > 0 ? { corsOrigins: CORS_ORIGINS } : {}),
37
+ admin: {
38
+ token: ADMIN_TOKEN,
39
+ keyStore,
40
+ ...(orgStore ? { orgStore } : {}),
41
+ },
41
42
  });
42
43
  console.log(`[engram-server] listening on :${PORT}`);
43
44
  export default { port: PORT, fetch: app.fetch };
44
- async function buildStores() {
45
- if (!DATABASE_URL) {
45
+ /**
46
+ * Single Postgres connection pool shared by both the engram tables
47
+ * (sessions/persons/keys/orgs via `pg-tagged`) and better-auth's
48
+ * Kysely adapter. Returns `null` in in-memory mode.
49
+ */
50
+ async function buildPool() {
51
+ if (!DATABASE_URL)
52
+ return null;
53
+ const { Pool } = await import("pg");
54
+ const url = new URL(DATABASE_URL);
55
+ return new Pool({
56
+ host: DATABASE_SOCKET_PATH ?? url.hostname,
57
+ port: DATABASE_SOCKET_PATH ? undefined : Number(url.port || 5432),
58
+ user: decodeURIComponent(url.username),
59
+ password: decodeURIComponent(url.password),
60
+ database: url.pathname.replace(/^\//, ""),
61
+ });
62
+ }
63
+ async function buildStores(pool) {
64
+ if (!pool) {
46
65
  console.warn("[engram-server] DATABASE_URL not set — in-memory mode (data is volatile)");
47
66
  const ks = new InMemoryKeyStore();
48
67
  const adapters = new Map();
@@ -58,10 +77,7 @@ async function buildStores() {
58
77
  },
59
78
  };
60
79
  }
61
- const { default: postgres } = await import("postgres");
62
- const sql = (DATABASE_SOCKET_PATH
63
- ? postgres(DATABASE_URL, { host: DATABASE_SOCKET_PATH })
64
- : postgres(DATABASE_URL));
80
+ const sql = pgSqlClient(pool);
65
81
  const ks = new PostgresKeyStore(sql);
66
82
  await ks.ensureSchema();
67
83
  // Session schema is workspace-independent — one bootstrap call is enough.
@@ -79,3 +95,45 @@ async function buildStores() {
79
95
  },
80
96
  };
81
97
  }
98
+ /**
99
+ * Wire better-auth + cookie resolver when the required env vars are
100
+ * set. Returns `{}` when auth is disabled (in-memory dev, or no
101
+ * Google OAuth configured) — the rest of the server still works,
102
+ * but only api-key callers can reach /v1.
103
+ */
104
+ async function buildCookieAuth(pool, getStorage) {
105
+ const secret = process.env.ENGRAM_AUTH_SECRET;
106
+ const baseURL = process.env.ENGRAM_AUTH_URL;
107
+ const googleId = process.env.ENGRAM_AUTH_GOOGLE_ID;
108
+ const googleSecret = process.env.ENGRAM_AUTH_GOOGLE_SECRET;
109
+ if (!secret || !baseURL || !googleId || !googleSecret) {
110
+ if (pool) {
111
+ console.warn("[engram-server] cookie auth disabled (set ENGRAM_AUTH_SECRET/URL/GOOGLE_ID/GOOGLE_SECRET to enable engram-web sign-in)");
112
+ }
113
+ return {};
114
+ }
115
+ if (!pool) {
116
+ console.warn("[engram-server] cookie auth requires DATABASE_URL — skipping");
117
+ return {};
118
+ }
119
+ const trusted = (process.env.ENGRAM_AUTH_TRUSTED_ORIGINS ?? "")
120
+ .split(",")
121
+ .map((s) => s.trim())
122
+ .filter(Boolean);
123
+ const auth = buildAuth({
124
+ pool,
125
+ secret,
126
+ baseURL,
127
+ google: { clientId: googleId, clientSecret: googleSecret },
128
+ ...(process.env.ENGRAM_AUTH_COOKIE_DOMAIN
129
+ ? { cookieDomain: process.env.ENGRAM_AUTH_COOKIE_DOMAIN }
130
+ : {}),
131
+ ...(trusted.length > 0 ? { trustedOrigins: trusted } : {}),
132
+ ...(process.env.ENGRAM_AUTH_EMAIL_DOMAIN
133
+ ? { emailDomain: process.env.ENGRAM_AUTH_EMAIL_DOMAIN }
134
+ : {}),
135
+ });
136
+ const orgStore = new PostgresOrgStore(pool);
137
+ const cookieAuth = makeCookieAuthResolver({ auth, pool, orgStore, getStorage });
138
+ return { authHandler: auth, cookieAuth, orgStore };
139
+ }
@@ -0,0 +1,2 @@
1
+ export declare const name = "0006-auth";
2
+ export declare const sql = "\n-- ============================================================\n-- Phase E1: human-user auth for engram-web\n--\n-- engram-server has always authenticated machine callers with\n-- per-workspace api keys (engram_api_keys). engram-web now wants\n-- to authenticate _people_ with Google SSO, so we add the\n-- standard better-auth quartet plus a workspace_members join.\n--\n-- All tables are namespaced `engram_auth_*` to keep them out of\n-- the `engram_*` domain (sessions / events / persons / aliases /\n-- identities) where `session` already means an agent trajectory.\n-- ============================================================\n\nCREATE TABLE IF NOT EXISTS engram_auth_users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n email_verified BOOLEAN NOT NULL DEFAULT FALSE,\n name TEXT,\n image TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_users_email ON engram_auth_users (email);\n\nCREATE TABLE IF NOT EXISTS engram_auth_sessions (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,\n token TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMPTZ NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n current_workspace_id TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_user ON engram_auth_sessions (user_id);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_token ON engram_auth_sessions (token);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_expires ON engram_auth_sessions (expires_at);\n\nCREATE TABLE IF NOT EXISTS engram_auth_accounts (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n access_token TEXT,\n refresh_token TEXT,\n id_token TEXT,\n access_token_expires_at TIMESTAMPTZ,\n refresh_token_expires_at TIMESTAMPTZ,\n scope TEXT,\n password TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n UNIQUE (provider_id, account_id)\n);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_accounts_user ON engram_auth_accounts (user_id);\n\nCREATE TABLE IF NOT EXISTS engram_auth_verifications (\n id TEXT PRIMARY KEY,\n identifier TEXT NOT NULL,\n value TEXT NOT NULL,\n expires_at TIMESTAMPTZ NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS idx_engram_auth_verifications_identifier\n ON engram_auth_verifications (identifier);\n\n-- workspace \u2194 user membership. A user can belong to many workspaces;\n-- a workspace can have many members. Roles are kept open-ended\n-- (validated at the app layer) since engram doesn't enforce any\n-- per-route authorization today \u2014 membership is the only check.\nCREATE TABLE IF NOT EXISTS engram_workspace_members (\n workspace_id TEXT NOT NULL REFERENCES engram_workspaces(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'member',\n joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (workspace_id, user_id)\n);\nCREATE INDEX IF NOT EXISTS idx_engram_workspace_members_user\n ON engram_workspace_members (user_id);\n";
@@ -0,0 +1,84 @@
1
+ export const name = "0006-auth";
2
+ export const sql = `
3
+ -- ============================================================
4
+ -- Phase E1: human-user auth for engram-web
5
+ --
6
+ -- engram-server has always authenticated machine callers with
7
+ -- per-workspace api keys (engram_api_keys). engram-web now wants
8
+ -- to authenticate _people_ with Google SSO, so we add the
9
+ -- standard better-auth quartet plus a workspace_members join.
10
+ --
11
+ -- All tables are namespaced \`engram_auth_*\` to keep them out of
12
+ -- the \`engram_*\` domain (sessions / events / persons / aliases /
13
+ -- identities) where \`session\` already means an agent trajectory.
14
+ -- ============================================================
15
+
16
+ CREATE TABLE IF NOT EXISTS engram_auth_users (
17
+ id TEXT PRIMARY KEY,
18
+ email TEXT NOT NULL UNIQUE,
19
+ email_verified BOOLEAN NOT NULL DEFAULT FALSE,
20
+ name TEXT,
21
+ image TEXT,
22
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
23
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
24
+ );
25
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_users_email ON engram_auth_users (email);
26
+
27
+ CREATE TABLE IF NOT EXISTS engram_auth_sessions (
28
+ id TEXT PRIMARY KEY,
29
+ user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,
30
+ token TEXT NOT NULL UNIQUE,
31
+ expires_at TIMESTAMPTZ NOT NULL,
32
+ ip_address TEXT,
33
+ user_agent TEXT,
34
+ current_workspace_id TEXT,
35
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
36
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_user ON engram_auth_sessions (user_id);
39
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_token ON engram_auth_sessions (token);
40
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_sessions_expires ON engram_auth_sessions (expires_at);
41
+
42
+ CREATE TABLE IF NOT EXISTS engram_auth_accounts (
43
+ id TEXT PRIMARY KEY,
44
+ user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,
45
+ account_id TEXT NOT NULL,
46
+ provider_id TEXT NOT NULL,
47
+ access_token TEXT,
48
+ refresh_token TEXT,
49
+ id_token TEXT,
50
+ access_token_expires_at TIMESTAMPTZ,
51
+ refresh_token_expires_at TIMESTAMPTZ,
52
+ scope TEXT,
53
+ password TEXT,
54
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
55
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
56
+ UNIQUE (provider_id, account_id)
57
+ );
58
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_accounts_user ON engram_auth_accounts (user_id);
59
+
60
+ CREATE TABLE IF NOT EXISTS engram_auth_verifications (
61
+ id TEXT PRIMARY KEY,
62
+ identifier TEXT NOT NULL,
63
+ value TEXT NOT NULL,
64
+ expires_at TIMESTAMPTZ NOT NULL,
65
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
67
+ );
68
+ CREATE INDEX IF NOT EXISTS idx_engram_auth_verifications_identifier
69
+ ON engram_auth_verifications (identifier);
70
+
71
+ -- workspace ↔ user membership. A user can belong to many workspaces;
72
+ -- a workspace can have many members. Roles are kept open-ended
73
+ -- (validated at the app layer) since engram doesn't enforce any
74
+ -- per-route authorization today — membership is the only check.
75
+ CREATE TABLE IF NOT EXISTS engram_workspace_members (
76
+ workspace_id TEXT NOT NULL REFERENCES engram_workspaces(id) ON DELETE CASCADE,
77
+ user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,
78
+ role TEXT NOT NULL DEFAULT 'member',
79
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
80
+ PRIMARY KEY (workspace_id, user_id)
81
+ );
82
+ CREATE INDEX IF NOT EXISTS idx_engram_workspace_members_user
83
+ ON engram_workspace_members (user_id);
84
+ `;
@@ -0,0 +1,2 @@
1
+ export declare const name = "0007-orgs";
2
+ export declare const sql = "\n-- ============================================================\n-- Wave G: orgs as the top-level grouping (Langfuse-style)\n--\n-- Before: cookie-auth users were members of individual workspaces\n-- (engram_workspace_members). That doesn't scale when a single\n-- dev team operates many tenant workspaces \u2014 every new tenant\n-- meant re-adding every dev. Move membership up one level so a\n-- user joins an org once and sees every workspace under it.\n--\n-- New shape:\n-- engram_orgs top-level grouping\n-- engram_workspaces now has org_id (NULL allowed only for\n-- tests / legacy api-key-only rows;\n-- cookie-auth requires the workspace's\n-- org membership)\n-- engram_org_members replaces engram_workspace_members\n-- ============================================================\n\nCREATE TABLE IF NOT EXISTS engram_orgs (\n id TEXT PRIMARY KEY,\n name TEXT,\n metadata JSONB NOT NULL DEFAULT '{}'::jsonb,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nALTER TABLE engram_workspaces ADD COLUMN IF NOT EXISTS org_id TEXT;\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'engram_workspaces_org_id_fkey'\n ) THEN\n ALTER TABLE engram_workspaces\n ADD CONSTRAINT engram_workspaces_org_id_fkey\n FOREIGN KEY (org_id) REFERENCES engram_orgs(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS idx_engram_workspaces_org\n ON engram_workspaces (org_id);\n\nCREATE TABLE IF NOT EXISTS engram_org_members (\n org_id TEXT NOT NULL REFERENCES engram_orgs(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES engram_auth_users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'member',\n joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (org_id, user_id)\n);\nCREATE INDEX IF NOT EXISTS idx_engram_org_members_user\n ON engram_org_members (user_id);\n\n-- workspace_members from 0006-auth is obsolete in the org-based\n-- model. Drop it; the only path to workspace access is via org\n-- membership.\nDROP TABLE IF EXISTS engram_workspace_members;\n";