@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/schemas.js
CHANGED
|
@@ -151,6 +151,25 @@ export const createWorkspaceSchema = z.object({
|
|
|
151
151
|
export const issueKeySchema = z.object({
|
|
152
152
|
name: z.string().optional(),
|
|
153
153
|
});
|
|
154
|
+
// --- Orgs (admin + self-serve share these bodies) --------------------
|
|
155
|
+
export const createOrgSchema = z.object({
|
|
156
|
+
id: z.string().optional(),
|
|
157
|
+
name: z.string().optional(),
|
|
158
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
159
|
+
});
|
|
160
|
+
export const orgPatchSchema = z.object({
|
|
161
|
+
name: z.string().optional(),
|
|
162
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
163
|
+
});
|
|
164
|
+
export const workspacePatchSchema = z.object({
|
|
165
|
+
name: z.string().optional(),
|
|
166
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
167
|
+
});
|
|
168
|
+
export const addMemberSchema = z.object({
|
|
169
|
+
userId: z.string().optional(),
|
|
170
|
+
email: z.string().optional(),
|
|
171
|
+
role: z.string().optional(),
|
|
172
|
+
});
|
|
154
173
|
// --- Helper ----------------------------------------------------------
|
|
155
174
|
/**
|
|
156
175
|
* Read and validate a JSON request body. Returns the parsed value, or a
|
package/dist/server.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { CookieAuthResolver } from "./auth-resolver";
|
|
|
4
4
|
import type { AuthResolver, Env } from "./context";
|
|
5
5
|
import type { OrgStore } from "./org-store";
|
|
6
6
|
import { type AdminOptions } from "./admin";
|
|
7
|
+
import type { KeyStore } from "./key-store";
|
|
7
8
|
export interface CreateServerOptions {
|
|
8
9
|
/** Resolves Bearer / X-Api-Key tokens into workspace contexts. */
|
|
9
10
|
auth: AuthResolver;
|
|
@@ -22,8 +23,20 @@ export interface CreateServerOptions {
|
|
|
22
23
|
* Optional: org store. When provided AND \`authHandler\` is set,
|
|
23
24
|
* exposes \`GET /v1/me/workspaces\` and \`GET /v1/me/orgs\` so
|
|
24
25
|
* engram-web can populate its org / workspace switcher.
|
|
26
|
+
*
|
|
27
|
+
* Combined with \`keyStore\` it also unlocks the cookie-auth
|
|
28
|
+
* \`/v1/orgs/:id/*\` self-service surface (members, workspaces,
|
|
29
|
+
* api keys) — see routes/orgs.ts.
|
|
25
30
|
*/
|
|
26
31
|
orgStore?: OrgStore;
|
|
32
|
+
/**
|
|
33
|
+
* Optional: key store, shared between the admin router and the
|
|
34
|
+
* cookie-auth orgs surface. The admin router takes its own
|
|
35
|
+
* reference (\`admin.keyStore\`) for the legacy / api-key flows;
|
|
36
|
+
* this top-level handle lets \`/v1/orgs/:id/*\` issue and revoke
|
|
37
|
+
* workspace keys without the admin token.
|
|
38
|
+
*/
|
|
39
|
+
keyStore?: KeyStore;
|
|
27
40
|
/**
|
|
28
41
|
* Generates session ids. Defaults to `crypto.randomUUID()`.
|
|
29
42
|
* Provide a custom function for deterministic ids in tests.
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,8 @@ import { sessionsRoutes } from "./routes/sessions";
|
|
|
7
7
|
import { personsRoutes } from "./routes/persons";
|
|
8
8
|
import { identitiesRoutes } from "./routes/identities";
|
|
9
9
|
import { searchRoutes } from "./routes/search";
|
|
10
|
+
import { orgsRoutes } from "./routes/orgs";
|
|
11
|
+
import { buildOpenApiDocument } from "./openapi";
|
|
10
12
|
/**
|
|
11
13
|
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
12
14
|
* middleware (request id + access log), the workspace auth gate, and mounts
|
|
@@ -91,6 +93,10 @@ export function createServer(opts) {
|
|
|
91
93
|
},
|
|
92
94
|
}));
|
|
93
95
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
96
|
+
// OpenAPI 3.1 document for the public /v1 surface. Served live
|
|
97
|
+
// from the same Zod schemas the server validates with, so it
|
|
98
|
+
// can't drift. engram-web's /docs renders this through Scalar.
|
|
99
|
+
app.get("/openapi.json", (c) => c.json(buildOpenApiDocument()));
|
|
94
100
|
if (opts.admin) {
|
|
95
101
|
app.route("/admin/v1", createAdminRouter(opts.admin));
|
|
96
102
|
}
|
|
@@ -100,33 +106,15 @@ export function createServer(opts) {
|
|
|
100
106
|
const handler = opts.authHandler;
|
|
101
107
|
app.all("/auth/*", (c) => handler.handler(c.req.raw));
|
|
102
108
|
}
|
|
103
|
-
// Workspace auth gate — every `/v1/*` route runs behind this. Tries
|
|
104
|
-
// api-key first; falls back to cookie session when configured. The
|
|
105
|
-
// Bearer header is reserved for api-keys here; engram-web uses the
|
|
106
|
-
// session cookie, not a Bearer.
|
|
107
|
-
app.use("/v1/*", async (c, next) => {
|
|
108
|
-
const apiKey = c.req.header("x-api-key") ??
|
|
109
|
-
c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
110
|
-
let ctx = apiKey ? await opts.auth(apiKey) : null;
|
|
111
|
-
if (!ctx && !apiKey && opts.cookieAuth) {
|
|
112
|
-
ctx = await opts.cookieAuth(c.req.raw);
|
|
113
|
-
}
|
|
114
|
-
if (!ctx)
|
|
115
|
-
return c.json({ error: "unauthorized" }, 401);
|
|
116
|
-
c.set("ctx", ctx);
|
|
117
|
-
await next();
|
|
118
|
-
});
|
|
119
|
-
// Identity probe — echoes the workspace the caller's auth
|
|
120
|
-
// resolves to. Cheap, used as a health/whoami by clients.
|
|
121
|
-
app.get("/v1/me", (c) => c.json({ workspaceId: c.var.ctx.workspaceId }));
|
|
122
109
|
// Cookie-auth helpers for engram-web: list every workspace / org
|
|
123
|
-
// the signed-in user can reach.
|
|
124
|
-
//
|
|
110
|
+
// the signed-in user can reach. Mounted BEFORE the workspace gate
|
|
111
|
+
// because these endpoints are how the UI chooses a workspace —
|
|
112
|
+
// gating them on x-workspace-id would deadlock new sign-ins.
|
|
125
113
|
if (opts.authHandler && opts.orgStore) {
|
|
126
|
-
const
|
|
114
|
+
const handler = opts.authHandler;
|
|
127
115
|
const orgStore = opts.orgStore;
|
|
128
116
|
const requireUser = async (req) => {
|
|
129
|
-
const s = await
|
|
117
|
+
const s = await handler.api.getSession({ headers: req.headers }).catch(() => null);
|
|
130
118
|
return s?.user ?? null;
|
|
131
119
|
};
|
|
132
120
|
app.get("/v1/me/workspaces", async (c) => {
|
|
@@ -147,7 +135,36 @@ export function createServer(opts) {
|
|
|
147
135
|
})));
|
|
148
136
|
return c.json({ orgs: orgs.filter((o) => o.id) });
|
|
149
137
|
});
|
|
138
|
+
// Org self-service surface (Wave G5): members, workspaces, api
|
|
139
|
+
// keys — same shape as the admin endpoints but cookie-auth +
|
|
140
|
+
// org-membership + role check.
|
|
141
|
+
if (opts.keyStore) {
|
|
142
|
+
app.route("/", orgsRoutes({
|
|
143
|
+
authHandler: handler,
|
|
144
|
+
orgStore,
|
|
145
|
+
keyStore: opts.keyStore,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
150
148
|
}
|
|
149
|
+
// Workspace auth gate — every other `/v1/*` route runs behind this.
|
|
150
|
+
// Tries api-key first; falls back to cookie session when configured.
|
|
151
|
+
// The Bearer header is reserved for api-keys here; engram-web uses
|
|
152
|
+
// the session cookie, not a Bearer.
|
|
153
|
+
app.use("/v1/*", async (c, next) => {
|
|
154
|
+
const apiKey = c.req.header("x-api-key") ??
|
|
155
|
+
c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
156
|
+
let ctx = apiKey ? await opts.auth(apiKey) : null;
|
|
157
|
+
if (!ctx && !apiKey && opts.cookieAuth) {
|
|
158
|
+
ctx = await opts.cookieAuth(c.req.raw);
|
|
159
|
+
}
|
|
160
|
+
if (!ctx)
|
|
161
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
162
|
+
c.set("ctx", ctx);
|
|
163
|
+
await next();
|
|
164
|
+
});
|
|
165
|
+
// Identity probe — echoes the workspace the caller's auth
|
|
166
|
+
// resolves to. Cheap, used as a health/whoami by clients.
|
|
167
|
+
app.get("/v1/me", (c) => c.json({ workspaceId: c.var.ctx.workspaceId }));
|
|
151
168
|
app.route("/v1", sessionsRoutes(cfg));
|
|
152
169
|
app.route("/v1", personsRoutes(cfg));
|
|
153
170
|
app.route("/v1", identitiesRoutes(cfg));
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org / workspace business logic shared between the admin-token surface
|
|
3
|
+
* (`/admin/v1/*`) and the cookie-auth self-service surface (`/v1/orgs/*`).
|
|
4
|
+
*
|
|
5
|
+
* Auth and HTTP framing stay in the route modules. This file owns:
|
|
6
|
+
* - the multi-step orchestrations (createWorkspace + setWorkspaceOrg
|
|
7
|
+
* + issueKey; email→userId lookup + upsertMember)
|
|
8
|
+
* - the safety invariants (workspace id validation, last-owner
|
|
9
|
+
* protection on member removal)
|
|
10
|
+
* - the canonical error → status mapping (OrgServiceError.status)
|
|
11
|
+
*
|
|
12
|
+
* Functions throw {@link OrgServiceError} for caller errors; route
|
|
13
|
+
* handlers catch it and translate to JSON. Storage errors that aren't
|
|
14
|
+
* known caller faults propagate.
|
|
15
|
+
*/
|
|
16
|
+
import { type IssuedKey, type KeyStore, type Workspace } from "../key-store";
|
|
17
|
+
import type { OrgMembershipRow, OrgRow, OrgStore } from "../org-store";
|
|
18
|
+
export type OrgServiceErrorStatus = 400 | 403 | 404;
|
|
19
|
+
/** Caller-fault errors with a stable code → status mapping. */
|
|
20
|
+
export declare class OrgServiceError extends Error {
|
|
21
|
+
readonly code: string;
|
|
22
|
+
readonly status: OrgServiceErrorStatus;
|
|
23
|
+
constructor(code: string, status: OrgServiceErrorStatus);
|
|
24
|
+
}
|
|
25
|
+
export interface OrgServiceDeps {
|
|
26
|
+
orgStore: OrgStore;
|
|
27
|
+
keyStore: KeyStore;
|
|
28
|
+
}
|
|
29
|
+
export declare function getOrgOrThrow(deps: OrgServiceDeps, id: string): Promise<OrgRow>;
|
|
30
|
+
export declare function createOrg(deps: OrgServiceDeps, body: {
|
|
31
|
+
id?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}): Promise<OrgRow>;
|
|
35
|
+
export declare function updateOrg(deps: OrgServiceDeps, id: string, body: {
|
|
36
|
+
name?: string;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}): Promise<OrgRow>;
|
|
39
|
+
export declare function deleteOrg(deps: OrgServiceDeps, id: string): Promise<void>;
|
|
40
|
+
export interface AddMemberInput {
|
|
41
|
+
userId?: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
role?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve an email or explicit userId to a membership row, upserting at
|
|
47
|
+
* the requested role. Used by both the admin and self-serve surfaces.
|
|
48
|
+
*/
|
|
49
|
+
export declare function addMember(deps: OrgServiceDeps, orgId: string, input: AddMemberInput): Promise<OrgMembershipRow>;
|
|
50
|
+
/**
|
|
51
|
+
* Remove a membership. Refuses to remove the org's sole remaining owner
|
|
52
|
+
* (would brick the org). Applies to admin-token callers too —
|
|
53
|
+
* `deleteOrg` is the path for tearing down an org entirely.
|
|
54
|
+
*/
|
|
55
|
+
export declare function removeMember(deps: OrgServiceDeps, orgId: string, userId: string): Promise<void>;
|
|
56
|
+
export interface CreateWorkspaceInput {
|
|
57
|
+
id?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
metadata?: Record<string, unknown>;
|
|
60
|
+
/** Whether to issue an initial API key. Default true. */
|
|
61
|
+
issueKey?: boolean;
|
|
62
|
+
/** Optional name applied to the initial API key. */
|
|
63
|
+
keyName?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface CreateWorkspaceResult {
|
|
66
|
+
workspace: Workspace & {
|
|
67
|
+
orgId: string;
|
|
68
|
+
};
|
|
69
|
+
/** Present unless `issueKey === false`. */
|
|
70
|
+
key?: IssuedKey;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Stand up a new workspace under an org in one round-trip:
|
|
74
|
+
* createWorkspace → setWorkspaceOrg → (optional) issueKey.
|
|
75
|
+
*
|
|
76
|
+
* Validates the workspace id shape up front so a bad id fails before
|
|
77
|
+
* touching either store. Returns the workspace with `orgId` stamped on
|
|
78
|
+
* so callers don't need a second lookup.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createWorkspaceUnderOrg(deps: OrgServiceDeps, orgId: string, body: CreateWorkspaceInput): Promise<CreateWorkspaceResult>;
|
|
81
|
+
export declare function updateOrgWorkspace(deps: OrgServiceDeps, orgId: string, wsId: string, body: {
|
|
82
|
+
name?: string;
|
|
83
|
+
metadata?: Record<string, unknown>;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
workspace: Workspace & {
|
|
86
|
+
orgId: string;
|
|
87
|
+
};
|
|
88
|
+
}>;
|
|
89
|
+
export declare function revokeWorkspaceKey(deps: OrgServiceDeps, wsId: string, keyId: string): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Throw `workspace_not_in_org` (404) if the workspace's `org_id` is
|
|
92
|
+
* not `orgId`. Used by both the admin-token and cookie-auth surfaces
|
|
93
|
+
* to scope per-workspace operations to the URL's org.
|
|
94
|
+
*/
|
|
95
|
+
export declare function requireWorkspaceInOrg(deps: OrgServiceDeps, orgId: string, wsId: string): Promise<void>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org / workspace business logic shared between the admin-token surface
|
|
3
|
+
* (`/admin/v1/*`) and the cookie-auth self-service surface (`/v1/orgs/*`).
|
|
4
|
+
*
|
|
5
|
+
* Auth and HTTP framing stay in the route modules. This file owns:
|
|
6
|
+
* - the multi-step orchestrations (createWorkspace + setWorkspaceOrg
|
|
7
|
+
* + issueKey; email→userId lookup + upsertMember)
|
|
8
|
+
* - the safety invariants (workspace id validation, last-owner
|
|
9
|
+
* protection on member removal)
|
|
10
|
+
* - the canonical error → status mapping (OrgServiceError.status)
|
|
11
|
+
*
|
|
12
|
+
* Functions throw {@link OrgServiceError} for caller errors; route
|
|
13
|
+
* handlers catch it and translate to JSON. Storage errors that aren't
|
|
14
|
+
* known caller faults propagate.
|
|
15
|
+
*/
|
|
16
|
+
import { isValidWorkspaceId, } from "../key-store";
|
|
17
|
+
/** Caller-fault errors with a stable code → status mapping. */
|
|
18
|
+
export class OrgServiceError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
status;
|
|
21
|
+
constructor(code, status) {
|
|
22
|
+
super(code);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.status = status;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// ---------- orgs ---------------------------------------------------
|
|
28
|
+
export async function getOrgOrThrow(deps, id) {
|
|
29
|
+
const org = await deps.orgStore.getOrg(id);
|
|
30
|
+
if (!org)
|
|
31
|
+
throw new OrgServiceError("org_not_found", 404);
|
|
32
|
+
return org;
|
|
33
|
+
}
|
|
34
|
+
export async function createOrg(deps, body) {
|
|
35
|
+
try {
|
|
36
|
+
return await deps.orgStore.createOrg({
|
|
37
|
+
...(body.id !== undefined ? { id: body.id } : {}),
|
|
38
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
39
|
+
...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
throw new OrgServiceError(e.message, 400);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function updateOrg(deps, id, body) {
|
|
47
|
+
try {
|
|
48
|
+
return await deps.orgStore.updateOrg(id, body);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (e.message === "org_not_found") {
|
|
52
|
+
throw new OrgServiceError("org_not_found", 404);
|
|
53
|
+
}
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function deleteOrg(deps, id) {
|
|
58
|
+
await getOrgOrThrow(deps, id);
|
|
59
|
+
await deps.orgStore.deleteOrg(id);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an email or explicit userId to a membership row, upserting at
|
|
63
|
+
* the requested role. Used by both the admin and self-serve surfaces.
|
|
64
|
+
*/
|
|
65
|
+
export async function addMember(deps, orgId, input) {
|
|
66
|
+
let userId = input.userId;
|
|
67
|
+
if (!userId && input.email) {
|
|
68
|
+
const u = await deps.orgStore.findUserByEmail(input.email);
|
|
69
|
+
if (!u)
|
|
70
|
+
throw new OrgServiceError("user_not_found", 404);
|
|
71
|
+
userId = u.id;
|
|
72
|
+
}
|
|
73
|
+
if (!userId)
|
|
74
|
+
throw new OrgServiceError("userId_or_email_required", 400);
|
|
75
|
+
return await deps.orgStore.upsertMember({
|
|
76
|
+
orgId,
|
|
77
|
+
userId,
|
|
78
|
+
...(input.role !== undefined ? { role: input.role } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Remove a membership. Refuses to remove the org's sole remaining owner
|
|
83
|
+
* (would brick the org). Applies to admin-token callers too —
|
|
84
|
+
* `deleteOrg` is the path for tearing down an org entirely.
|
|
85
|
+
*/
|
|
86
|
+
export async function removeMember(deps, orgId, userId) {
|
|
87
|
+
const members = await deps.orgStore.listMembers(orgId);
|
|
88
|
+
const owners = members.filter((m) => m.role === "owner");
|
|
89
|
+
if (owners.length === 1 && owners[0].userId === userId) {
|
|
90
|
+
throw new OrgServiceError("cannot_remove_last_owner", 400);
|
|
91
|
+
}
|
|
92
|
+
await deps.orgStore.removeMember(orgId, userId);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stand up a new workspace under an org in one round-trip:
|
|
96
|
+
* createWorkspace → setWorkspaceOrg → (optional) issueKey.
|
|
97
|
+
*
|
|
98
|
+
* Validates the workspace id shape up front so a bad id fails before
|
|
99
|
+
* touching either store. Returns the workspace with `orgId` stamped on
|
|
100
|
+
* so callers don't need a second lookup.
|
|
101
|
+
*/
|
|
102
|
+
export async function createWorkspaceUnderOrg(deps, orgId, body) {
|
|
103
|
+
if (body.id !== undefined && !isValidWorkspaceId(body.id)) {
|
|
104
|
+
throw new OrgServiceError("invalid_workspace_id", 400);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const ws = await deps.keyStore.createWorkspace({
|
|
108
|
+
...(body.id !== undefined ? { id: body.id } : {}),
|
|
109
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
110
|
+
...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
|
|
111
|
+
});
|
|
112
|
+
await deps.orgStore.setWorkspaceOrg(ws.id, orgId);
|
|
113
|
+
if (body.issueKey === false) {
|
|
114
|
+
return { workspace: { ...ws, orgId } };
|
|
115
|
+
}
|
|
116
|
+
const key = await deps.keyStore.issueKey(ws.id, {
|
|
117
|
+
...(body.keyName !== undefined ? { name: body.keyName } : {}),
|
|
118
|
+
});
|
|
119
|
+
return { workspace: { ...ws, orgId }, key };
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
if (e instanceof OrgServiceError)
|
|
123
|
+
throw e;
|
|
124
|
+
throw new OrgServiceError(e.message, 400);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export async function updateOrgWorkspace(deps, orgId, wsId, body) {
|
|
128
|
+
try {
|
|
129
|
+
const ws = await deps.keyStore.updateWorkspace(wsId, body);
|
|
130
|
+
return { workspace: { ...ws, orgId } };
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
if (e.message === "workspace_not_found") {
|
|
134
|
+
throw new OrgServiceError("workspace_not_found", 404);
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function revokeWorkspaceKey(deps, wsId, keyId) {
|
|
140
|
+
try {
|
|
141
|
+
await deps.keyStore.revokeKey(wsId, keyId);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (e.message === "key_not_found") {
|
|
145
|
+
throw new OrgServiceError("key_not_found", 404);
|
|
146
|
+
}
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Throw `workspace_not_in_org` (404) if the workspace's `org_id` is
|
|
152
|
+
* not `orgId`. Used by both the admin-token and cookie-auth surfaces
|
|
153
|
+
* to scope per-workspace operations to the URL's org.
|
|
154
|
+
*/
|
|
155
|
+
export async function requireWorkspaceInOrg(deps, orgId, wsId) {
|
|
156
|
+
if (!(await deps.orgStore.workspaceInOrg(orgId, wsId))) {
|
|
157
|
+
throw new OrgServiceError("workspace_not_in_org", 404);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -149,3 +149,9 @@ export interface SessionRow {
|
|
|
149
149
|
trigger_event_id?: string;
|
|
150
150
|
}
|
|
151
151
|
export declare function foldEvents(row: SessionRow, events: SessionEvent[], now: Date): Session;
|
|
152
|
+
/**
|
|
153
|
+
* Allocate a fresh person id (`p_` + 8 alphanumeric chars). Cryptographically
|
|
154
|
+
* random via `crypto.getRandomValues`; shared between every adapter so id
|
|
155
|
+
* shape and entropy stay aligned.
|
|
156
|
+
*/
|
|
157
|
+
export declare function newPersonId(): string;
|
package/dist/storage.js
CHANGED
|
@@ -38,3 +38,17 @@ export function foldEvents(row, events, now) {
|
|
|
38
38
|
updated_at: row.updatedAt,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
const PERSON_ID_ALPHA = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
42
|
+
/**
|
|
43
|
+
* Allocate a fresh person id (`p_` + 8 alphanumeric chars). Cryptographically
|
|
44
|
+
* random via `crypto.getRandomValues`; shared between every adapter so id
|
|
45
|
+
* shape and entropy stay aligned.
|
|
46
|
+
*/
|
|
47
|
+
export function newPersonId() {
|
|
48
|
+
const buf = new Uint8Array(8);
|
|
49
|
+
crypto.getRandomValues(buf);
|
|
50
|
+
let out = "p_";
|
|
51
|
+
for (const b of buf)
|
|
52
|
+
out += PERSON_ID_ALPHA[b % PERSON_ID_ALPHA.length];
|
|
53
|
+
return out;
|
|
54
|
+
}
|