@hexis-ai/engram-server 0.12.0 → 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.
- package/dist/adapters/memory-key-store.d.ts +4 -0
- package/dist/adapters/memory-key-store.js +12 -0
- package/dist/adapters/memory.js +47 -66
- package/dist/adapters/pg-tagged.d.ts +18 -0
- package/dist/adapters/pg-tagged.js +29 -0
- package/dist/adapters/postgres-key-store.d.ts +4 -0
- package/dist/adapters/postgres-key-store.js +14 -3
- package/dist/adapters/postgres-org-store.d.ts +5 -0
- package/dist/adapters/postgres-org-store.js +23 -5
- package/dist/adapters/postgres.js +57 -80
- package/dist/adapters/util.d.ts +27 -0
- package/dist/adapters/util.js +47 -0
- package/dist/admin.js +78 -89
- package/dist/key-store.d.ts +5 -0
- package/dist/main.js +29 -44
- package/dist/openapi.js +340 -3
- package/dist/org-store.d.ts +7 -0
- package/dist/routes/orgs.d.ts +27 -0
- package/dist/routes/orgs.js +185 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.js +19 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +40 -23
- package/dist/services/orgs.d.ts +95 -0
- package/dist/services/orgs.js +159 -0
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +14 -0
- package/openapi.json +1279 -1
- package/package.json +3 -12
package/dist/admin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { isValidWorkspaceId } from "./key-store";
|
|
3
3
|
import { createWorkspaceSchema, issueKeySchema, parseJsonBody } from "./schemas";
|
|
4
|
+
import { addMember, createOrg, createWorkspaceUnderOrg, deleteOrg, getOrgOrThrow, OrgServiceError, removeMember, requireWorkspaceInOrg, revokeWorkspaceKey, } from "./services/orgs";
|
|
4
5
|
/**
|
|
5
6
|
* Build the admin sub-router. Mount under \`/admin/v1\`.
|
|
6
7
|
*
|
|
@@ -119,117 +120,105 @@ export function createAdminRouter(opts) {
|
|
|
119
120
|
const orgStore = opts.orgStore;
|
|
120
121
|
if (!orgStore)
|
|
121
122
|
return app;
|
|
122
|
-
|
|
123
|
+
const deps = { orgStore, keyStore: opts.keyStore };
|
|
124
|
+
app.post("/orgs", async (c) => runService(c, async () => {
|
|
123
125
|
const body = (await c.req.json().catch(() => ({})));
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
...(body.name !== undefined ? { name: body.name } : {}),
|
|
128
|
-
...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
|
|
129
|
-
});
|
|
130
|
-
return c.json({ org });
|
|
131
|
-
}
|
|
132
|
-
catch (e) {
|
|
133
|
-
return c.json({ error: e.message }, 400);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
126
|
+
const org = await createOrg(deps, body);
|
|
127
|
+
return c.json({ org });
|
|
128
|
+
}));
|
|
136
129
|
app.get("/orgs", async (c) => {
|
|
137
130
|
const orgs = await orgStore.listOrgs();
|
|
138
131
|
return c.json({ orgs });
|
|
139
132
|
});
|
|
140
|
-
app.get("/orgs/:id", async (c) => {
|
|
141
|
-
const org = await
|
|
142
|
-
if (!org)
|
|
143
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
133
|
+
app.get("/orgs/:id", async (c) => runService(c, async () => {
|
|
134
|
+
const org = await getOrgOrThrow(deps, c.req.param("id"));
|
|
144
135
|
return c.json({ org });
|
|
145
|
-
});
|
|
146
|
-
app.delete("/orgs/:id", async (c) => {
|
|
147
|
-
|
|
148
|
-
const org = await orgStore.getOrg(id);
|
|
149
|
-
if (!org)
|
|
150
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
151
|
-
await orgStore.deleteOrg(id);
|
|
136
|
+
}));
|
|
137
|
+
app.delete("/orgs/:id", async (c) => runService(c, async () => {
|
|
138
|
+
await deleteOrg(deps, c.req.param("id"));
|
|
152
139
|
return c.body(null, 204);
|
|
153
|
-
});
|
|
140
|
+
}));
|
|
154
141
|
// ----- org members ------------------------------------------
|
|
155
|
-
app.get("/orgs/:id/members", async (c) => {
|
|
142
|
+
app.get("/orgs/:id/members", async (c) => runService(c, async () => {
|
|
156
143
|
const id = c.req.param("id");
|
|
157
|
-
|
|
158
|
-
if (!org)
|
|
159
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
144
|
+
await getOrgOrThrow(deps, id);
|
|
160
145
|
return c.json({ members: await orgStore.listMembers(id) });
|
|
161
|
-
});
|
|
162
|
-
app.post("/orgs/:id/members", async (c) => {
|
|
146
|
+
}));
|
|
147
|
+
app.post("/orgs/:id/members", async (c) => runService(c, async () => {
|
|
163
148
|
const orgId = c.req.param("id");
|
|
164
|
-
|
|
165
|
-
if (!org)
|
|
166
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
149
|
+
await getOrgOrThrow(deps, orgId);
|
|
167
150
|
const body = (await c.req.json().catch(() => ({})));
|
|
168
|
-
|
|
169
|
-
if (!userId && body.email) {
|
|
170
|
-
const u = await orgStore.findUserByEmail(body.email);
|
|
171
|
-
if (!u)
|
|
172
|
-
return c.json({ error: "user_not_found" }, 404);
|
|
173
|
-
userId = u.id;
|
|
174
|
-
}
|
|
175
|
-
if (!userId)
|
|
176
|
-
return c.json({ error: "userId_or_email_required" }, 400);
|
|
177
|
-
const member = await orgStore.upsertMember({
|
|
178
|
-
orgId,
|
|
179
|
-
userId,
|
|
180
|
-
...(body.role !== undefined ? { role: body.role } : {}),
|
|
181
|
-
});
|
|
151
|
+
const member = await addMember(deps, orgId, body);
|
|
182
152
|
return c.json({ member });
|
|
183
|
-
});
|
|
184
|
-
app.delete("/orgs/:id/members/:userId", async (c) => {
|
|
153
|
+
}));
|
|
154
|
+
app.delete("/orgs/:id/members/:userId", async (c) => runService(c, async () => {
|
|
185
155
|
const orgId = c.req.param("id");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
189
|
-
await orgStore.removeMember(orgId, c.req.param("userId"));
|
|
156
|
+
await getOrgOrThrow(deps, orgId);
|
|
157
|
+
await removeMember(deps, orgId, c.req.param("userId"));
|
|
190
158
|
return c.body(null, 204);
|
|
191
|
-
});
|
|
159
|
+
}));
|
|
192
160
|
// ----- org workspaces ---------------------------------------
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
app.post("/orgs/:id/workspaces", async (c) => {
|
|
161
|
+
// Stands up a tenant in one round-trip. createWorkspaceUnderOrg
|
|
162
|
+
// wraps createWorkspace + setWorkspaceOrg + issueKey and applies
|
|
163
|
+
// the same id validation as the legacy /workspaces POST.
|
|
164
|
+
app.post("/orgs/:id/workspaces", async (c) => runService(c, async () => {
|
|
197
165
|
const orgId = c.req.param("id");
|
|
198
|
-
|
|
199
|
-
if (!org)
|
|
200
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
166
|
+
await getOrgOrThrow(deps, orgId);
|
|
201
167
|
const body = await parseJsonBody(c, createWorkspaceSchema);
|
|
202
168
|
if (body instanceof Response)
|
|
203
169
|
return body;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const ws = await opts.keyStore.createWorkspace({
|
|
209
|
-
...(body.id !== undefined ? { id: body.id } : {}),
|
|
210
|
-
...(body.name !== undefined ? { name: body.name } : {}),
|
|
211
|
-
...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
|
|
212
|
-
});
|
|
213
|
-
await orgStore.setWorkspaceOrg(ws.id, orgId);
|
|
214
|
-
if (body.issueKey === false) {
|
|
215
|
-
return c.json({ workspace: { ...ws, orgId } });
|
|
216
|
-
}
|
|
217
|
-
const key = await opts.keyStore.issueKey(ws.id, {
|
|
218
|
-
...(body.keyName !== undefined ? { name: body.keyName } : {}),
|
|
219
|
-
});
|
|
220
|
-
return c.json({ workspace: { ...ws, orgId }, key });
|
|
221
|
-
}
|
|
222
|
-
catch (e) {
|
|
223
|
-
return c.json({ error: e.message }, 400);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
app.get("/orgs/:id/workspaces", async (c) => {
|
|
170
|
+
return c.json(await createWorkspaceUnderOrg(deps, orgId, body));
|
|
171
|
+
}));
|
|
172
|
+
app.get("/orgs/:id/workspaces", async (c) => runService(c, async () => {
|
|
227
173
|
const orgId = c.req.param("id");
|
|
228
|
-
|
|
229
|
-
if (!org)
|
|
230
|
-
return c.json({ error: "org_not_found" }, 404);
|
|
174
|
+
await getOrgOrThrow(deps, orgId);
|
|
231
175
|
const workspaces = await orgStore.listWorkspacesForOrg(orgId);
|
|
232
176
|
return c.json({ workspaces });
|
|
233
|
-
});
|
|
177
|
+
}));
|
|
178
|
+
// ----- per-workspace api keys (scoped to an org) -----------
|
|
179
|
+
// Org-scoped equivalent of the legacy /admin/v1/workspaces/:id/keys
|
|
180
|
+
// routes. monet uses these for key rotation after the initial
|
|
181
|
+
// createWorkspaceUnderOrg + key issuance.
|
|
182
|
+
app.get("/orgs/:id/workspaces/:wsId/keys", async (c) => runService(c, async () => {
|
|
183
|
+
const orgId = c.req.param("id");
|
|
184
|
+
const wsId = c.req.param("wsId");
|
|
185
|
+
await getOrgOrThrow(deps, orgId);
|
|
186
|
+
await requireWorkspaceInOrg(deps, orgId, wsId);
|
|
187
|
+
return c.json({ keys: await opts.keyStore.listKeys(wsId) });
|
|
188
|
+
}));
|
|
189
|
+
app.post("/orgs/:id/workspaces/:wsId/keys", async (c) => runService(c, async () => {
|
|
190
|
+
const orgId = c.req.param("id");
|
|
191
|
+
const wsId = c.req.param("wsId");
|
|
192
|
+
await getOrgOrThrow(deps, orgId);
|
|
193
|
+
await requireWorkspaceInOrg(deps, orgId, wsId);
|
|
194
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
195
|
+
const parsed = issueKeySchema.safeParse(raw);
|
|
196
|
+
if (!parsed.success) {
|
|
197
|
+
return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
|
|
198
|
+
}
|
|
199
|
+
const key = await opts.keyStore.issueKey(wsId, {
|
|
200
|
+
...(parsed.data.name !== undefined ? { name: parsed.data.name } : {}),
|
|
201
|
+
});
|
|
202
|
+
return c.json(key);
|
|
203
|
+
}));
|
|
204
|
+
app.delete("/orgs/:id/workspaces/:wsId/keys/:keyId", async (c) => runService(c, async () => {
|
|
205
|
+
const orgId = c.req.param("id");
|
|
206
|
+
const wsId = c.req.param("wsId");
|
|
207
|
+
await getOrgOrThrow(deps, orgId);
|
|
208
|
+
await requireWorkspaceInOrg(deps, orgId, wsId);
|
|
209
|
+
await revokeWorkspaceKey(deps, wsId, c.req.param("keyId"));
|
|
210
|
+
return c.body(null, 204);
|
|
211
|
+
}));
|
|
234
212
|
return app;
|
|
235
213
|
}
|
|
214
|
+
/** Run a service call, mapping OrgServiceError to a JSON response. */
|
|
215
|
+
async function runService(c, fn) {
|
|
216
|
+
try {
|
|
217
|
+
return await fn();
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
if (e instanceof OrgServiceError)
|
|
221
|
+
return c.json({ error: e.code }, e.status);
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/key-store.d.ts
CHANGED
|
@@ -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,32 +1,9 @@
|
|
|
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
|
-
* Optional env (engram-web cookie auth — see ./auth.ts for the full set):
|
|
16
|
-
* ENGRAM_AUTH_SECRET, ENGRAM_AUTH_URL,
|
|
17
|
-
* ENGRAM_AUTH_GOOGLE_ID, ENGRAM_AUTH_GOOGLE_SECRET,
|
|
18
|
-
* ENGRAM_AUTH_COOKIE_DOMAIN, ENGRAM_AUTH_TRUSTED_ORIGINS,
|
|
19
|
-
* ENGRAM_AUTH_EMAIL_DOMAIN
|
|
20
|
-
*
|
|
21
|
-
* Workspaces and their API keys are provisioned exclusively through the
|
|
22
|
-
* admin API. There is no single-tenant fallback — every caller must hold a
|
|
23
|
-
* workspace-scoped key issued by `POST /admin/v1/workspaces`.
|
|
24
|
-
*/
|
|
25
1
|
import { createServer } from "./server";
|
|
26
2
|
import { InMemoryAdapter } from "./adapters/memory";
|
|
27
3
|
import { PostgresAdapter } from "./adapters/postgres";
|
|
28
4
|
import { InMemoryKeyStore } from "./adapters/memory-key-store";
|
|
29
5
|
import { PostgresKeyStore } from "./adapters/postgres-key-store";
|
|
6
|
+
import { pgSqlClient } from "./adapters/pg-tagged";
|
|
30
7
|
import { buildAuth } from "./auth";
|
|
31
8
|
import { makeCookieAuthResolver } from "./auth-resolver";
|
|
32
9
|
import { PostgresOrgStore } from "./adapters/postgres-org-store";
|
|
@@ -38,8 +15,9 @@ if (!ADMIN_TOKEN) {
|
|
|
38
15
|
console.error("[engram-server] ENGRAM_ADMIN_TOKEN is required");
|
|
39
16
|
process.exit(1);
|
|
40
17
|
}
|
|
41
|
-
const
|
|
42
|
-
const {
|
|
18
|
+
const pool = await buildPool();
|
|
19
|
+
const { keyStore, getStorage } = await buildStores(pool);
|
|
20
|
+
const { authHandler, cookieAuth, orgStore } = await buildCookieAuth(pool, getStorage);
|
|
43
21
|
const CORS_ORIGINS = (process.env.ENGRAM_CORS_ORIGINS ?? "")
|
|
44
22
|
.split(",")
|
|
45
23
|
.map((s) => s.trim())
|
|
@@ -54,6 +32,7 @@ const app = createServer({
|
|
|
54
32
|
...(authHandler ? { authHandler } : {}),
|
|
55
33
|
...(cookieAuth ? { cookieAuth } : {}),
|
|
56
34
|
...(orgStore ? { orgStore } : {}),
|
|
35
|
+
keyStore,
|
|
57
36
|
...(CORS_ORIGINS.length > 0 ? { corsOrigins: CORS_ORIGINS } : {}),
|
|
58
37
|
admin: {
|
|
59
38
|
token: ADMIN_TOKEN,
|
|
@@ -63,8 +42,26 @@ const app = createServer({
|
|
|
63
42
|
});
|
|
64
43
|
console.log(`[engram-server] listening on :${PORT}`);
|
|
65
44
|
export default { port: PORT, fetch: app.fetch };
|
|
66
|
-
|
|
67
|
-
|
|
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) {
|
|
68
65
|
console.warn("[engram-server] DATABASE_URL not set — in-memory mode (data is volatile)");
|
|
69
66
|
const ks = new InMemoryKeyStore();
|
|
70
67
|
const adapters = new Map();
|
|
@@ -80,10 +77,7 @@ async function buildStores() {
|
|
|
80
77
|
},
|
|
81
78
|
};
|
|
82
79
|
}
|
|
83
|
-
const
|
|
84
|
-
const sql = (DATABASE_SOCKET_PATH
|
|
85
|
-
? postgres(DATABASE_URL, { host: DATABASE_SOCKET_PATH })
|
|
86
|
-
: postgres(DATABASE_URL));
|
|
80
|
+
const sql = pgSqlClient(pool);
|
|
87
81
|
const ks = new PostgresKeyStore(sql);
|
|
88
82
|
await ks.ensureSchema();
|
|
89
83
|
// Session schema is workspace-independent — one bootstrap call is enough.
|
|
@@ -107,30 +101,21 @@ async function buildStores() {
|
|
|
107
101
|
* Google OAuth configured) — the rest of the server still works,
|
|
108
102
|
* but only api-key callers can reach /v1.
|
|
109
103
|
*/
|
|
110
|
-
async function buildCookieAuth(getStorage) {
|
|
104
|
+
async function buildCookieAuth(pool, getStorage) {
|
|
111
105
|
const secret = process.env.ENGRAM_AUTH_SECRET;
|
|
112
106
|
const baseURL = process.env.ENGRAM_AUTH_URL;
|
|
113
107
|
const googleId = process.env.ENGRAM_AUTH_GOOGLE_ID;
|
|
114
108
|
const googleSecret = process.env.ENGRAM_AUTH_GOOGLE_SECRET;
|
|
115
109
|
if (!secret || !baseURL || !googleId || !googleSecret) {
|
|
116
|
-
if (
|
|
110
|
+
if (pool) {
|
|
117
111
|
console.warn("[engram-server] cookie auth disabled (set ENGRAM_AUTH_SECRET/URL/GOOGLE_ID/GOOGLE_SECRET to enable engram-web sign-in)");
|
|
118
112
|
}
|
|
119
113
|
return {};
|
|
120
114
|
}
|
|
121
|
-
if (!
|
|
115
|
+
if (!pool) {
|
|
122
116
|
console.warn("[engram-server] cookie auth requires DATABASE_URL — skipping");
|
|
123
117
|
return {};
|
|
124
118
|
}
|
|
125
|
-
const { Pool } = await import("pg");
|
|
126
|
-
const url = new URL(DATABASE_URL);
|
|
127
|
-
const pool = new Pool({
|
|
128
|
-
host: DATABASE_SOCKET_PATH ?? url.hostname,
|
|
129
|
-
port: DATABASE_SOCKET_PATH ? undefined : Number(url.port || 5432),
|
|
130
|
-
user: decodeURIComponent(url.username),
|
|
131
|
-
password: decodeURIComponent(url.password),
|
|
132
|
-
database: url.pathname.replace(/^\//, ""),
|
|
133
|
-
});
|
|
134
119
|
const trusted = (process.env.ENGRAM_AUTH_TRUSTED_ORIGINS ?? "")
|
|
135
120
|
.split(",")
|
|
136
121
|
.map((s) => s.trim())
|