@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/openapi.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * codes stay as-is.
19
19
  */
20
20
  import { z } from "zod";
21
- import { createWorkspaceSchema, eventBatchSchema, issueKeySchema, personCreateSchema, personUpdateSchema, searchRequestSchema, sessionInitSchema, } from "./schemas";
21
+ import { addMemberSchema, createOrgSchema, createWorkspaceSchema, eventBatchSchema, issueKeySchema, orgPatchSchema, personCreateSchema, personUpdateSchema, searchRequestSchema, sessionInitSchema, sessionUpdateSchema, aliasUpsertSchema, identityUpsertSchema, workspacePatchSchema, } from "./schemas";
22
22
  /** Convert a Zod schema to a JSON Schema object for an OpenAPI component. */
23
23
  function toComponent(schema) {
24
24
  const js = z.toJSONSchema(schema);
@@ -37,7 +37,7 @@ const pathParam = (name, description) => ({
37
37
  schema: { type: "string" },
38
38
  description,
39
39
  });
40
- const queryParam = (name, description, schema = { type: "string" }) => ({ name, in: "query", required: false, schema, description });
40
+ const queryParam = (name, description, schema = { type: "string" }, required = false) => ({ name, in: "query", required, schema, description });
41
41
  const res = (description) => ({ description });
42
42
  /** Default security: a workspace API key. `/admin/v1` paths override this. */
43
43
  const workspaceAuth = [{ workspaceKey: [] }];
@@ -54,14 +54,23 @@ const adminAuth = [{ adminToken: [] }];
54
54
  // ---------------------------------------------------------------------
55
55
  /** Every tag, in sidebar order, with a Japanese description. */
56
56
  const TAG_DEFS = [
57
- { name: "Me", description: "識別プローブ" },
57
+ { name: "Me", description: "識別プローブ・閲覧可能な workspace/org 一覧" },
58
58
  { name: "Sessions", description: "セッションの作成・取得・イベント追加" },
59
59
  { name: "Search", description: "ワークスペースコーパスへのスコアリング検索" },
60
60
  { name: "Persons", description: "person の作成・更新・検索" },
61
+ { name: "Aliases", description: "ワークスペース横断の alias 解決" },
61
62
  {
62
63
  name: "Identities",
63
64
  description: "外部 ID(slack: / email: など)の resolve と upsert",
64
65
  },
66
+ {
67
+ name: "Orgs (self-serve)",
68
+ description: "engram-web からの cookie 認証 + org-member ロールでの org / workspace / API キー管理",
69
+ },
70
+ {
71
+ name: "Orgs (admin)",
72
+ description: "プラットフォーム管理者トークンでの org 管理",
73
+ },
65
74
  {
66
75
  name: "Workspaces (admin)",
67
76
  description: "ワークスペースの管理(管理者トークン必須)",
@@ -99,6 +108,24 @@ function buildPaths() {
99
108
  },
100
109
  },
101
110
  }),
111
+ "/v1/me/workspaces": tagged("Me", {
112
+ get: {
113
+ summary: "サインイン中のユーザーが閲覧できる workspace 一覧。cookie 認証専用 — workspace 選択前にも到達できるよう workspace ゲートの前段に置かれている。",
114
+ responses: {
115
+ "200": res("workspace 一覧"),
116
+ "401": res("認証エラー"),
117
+ },
118
+ },
119
+ }),
120
+ "/v1/me/orgs": tagged("Me", {
121
+ get: {
122
+ summary: "サインイン中のユーザーが member として所属する org 一覧(自分の role 付き)。cookie 認証専用。",
123
+ responses: {
124
+ "200": res("org 一覧"),
125
+ "401": res("認証エラー"),
126
+ },
127
+ },
128
+ }),
102
129
  "/v1/sessions": tagged("Sessions", {
103
130
  post: {
104
131
  summary: "セッションを作成する。",
@@ -282,6 +309,16 @@ function buildPaths() {
282
309
  },
283
310
  },
284
311
  }),
312
+ "/v1/aliases": tagged("Aliases", {
313
+ get: {
314
+ summary: "ワークスペース全体で alias 名を case-insensitive に逆引き。同名の alias を持つ複数の person を `last_used` desc で返す。`persons` map も同梱。",
315
+ parameters: [queryParam("name", "alias 名(URL-encoded)。", { type: "string" }, true)],
316
+ responses: {
317
+ "200": res("alias 一覧 + persons map"),
318
+ "401": res("認証エラー"),
319
+ },
320
+ },
321
+ }),
285
322
  "/v1/identities/{ref}": tagged("Identities", {
286
323
  put: {
287
324
  summary: "ref(例: `slack:U12345`、`email:foo@bar.com`)で identity を upsert する。",
@@ -304,6 +341,299 @@ function buildPaths() {
304
341
  },
305
342
  },
306
343
  }),
344
+ // ------------------------------------------------------------------
345
+ // Self-service org surface (Wave G5). Cookie auth + org membership.
346
+ // engram-web の settings UI から呼ばれる。/admin/v1/orgs/* と同じ
347
+ // 操作を「自分が所属する org に絞って」叩けるエンドポイント群。
348
+ // ------------------------------------------------------------------
349
+ "/v1/orgs/{id}": tagged("Orgs (self-serve)", {
350
+ get: {
351
+ summary: "自分が member の org を取得する(自分の role 付き)。",
352
+ parameters: [pathParam("id", "org id。")],
353
+ responses: {
354
+ "200": res("org + role"),
355
+ "403": res("この org の member ではない"),
356
+ "404": res("org が見つからない"),
357
+ "401": res("認証エラー"),
358
+ },
359
+ },
360
+ patch: {
361
+ summary: "org の name / metadata を更新する(owner / admin のみ)。",
362
+ parameters: [pathParam("id", "org id。")],
363
+ requestBody: jsonBody("OrgPatch"),
364
+ responses: {
365
+ "200": res("更新後の org"),
366
+ "403": res("権限不足(owner / admin が必要)"),
367
+ "404": res("org が見つからない"),
368
+ "401": res("認証エラー"),
369
+ },
370
+ },
371
+ }),
372
+ "/v1/orgs/{id}/members": tagged("Orgs (self-serve)", {
373
+ get: {
374
+ summary: "org の member 一覧。",
375
+ parameters: [pathParam("id", "org id。")],
376
+ responses: {
377
+ "200": res("member 一覧"),
378
+ "403": res("この org の member ではない"),
379
+ "401": res("認証エラー"),
380
+ },
381
+ },
382
+ post: {
383
+ summary: "member を追加する(owner / admin のみ)。`email` か `userId` のいずれかを必須で渡す。",
384
+ parameters: [pathParam("id", "org id。")],
385
+ requestBody: jsonBody("AddMember"),
386
+ responses: {
387
+ "200": res("追加された membership"),
388
+ "400": res("`userId` も `email` も無い、または body が不正"),
389
+ "403": res("権限不足"),
390
+ "404": res("email から user を解決できない"),
391
+ "401": res("認証エラー"),
392
+ },
393
+ },
394
+ }),
395
+ "/v1/orgs/{id}/members/{userId}": tagged("Orgs (self-serve)", {
396
+ delete: {
397
+ summary: "member を削除する(owner / admin のみ)。最後の owner は削除拒否(`cannot_remove_last_owner`)。",
398
+ parameters: [pathParam("id", "org id。"), pathParam("userId", "user id。")],
399
+ responses: {
400
+ "204": res("削除完了"),
401
+ "400": res("最後の owner は削除できない"),
402
+ "403": res("権限不足"),
403
+ "401": res("認証エラー"),
404
+ },
405
+ },
406
+ }),
407
+ "/v1/orgs/{id}/workspaces": tagged("Orgs (self-serve)", {
408
+ get: {
409
+ summary: "org の workspace 一覧。",
410
+ parameters: [pathParam("id", "org id。")],
411
+ responses: {
412
+ "200": res("workspace 一覧"),
413
+ "403": res("この org の member ではない"),
414
+ "401": res("認証エラー"),
415
+ },
416
+ },
417
+ post: {
418
+ summary: "workspace を作成し、(issueKey=false でなければ)初期 API キーも発行する。owner / admin のみ。",
419
+ parameters: [pathParam("id", "org id。")],
420
+ requestBody: jsonBody("CreateWorkspace"),
421
+ responses: {
422
+ "200": res("workspace(issueKey=false でない限り key も含む)"),
423
+ "400": res("body または workspace id が不正"),
424
+ "403": res("権限不足"),
425
+ "401": res("認証エラー"),
426
+ },
427
+ },
428
+ }),
429
+ "/v1/orgs/{id}/workspaces/{wsId}": tagged("Orgs (self-serve)", {
430
+ patch: {
431
+ summary: "workspace の name / metadata を更新する(owner / admin のみ)。",
432
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
433
+ requestBody: jsonBody("WorkspacePatch"),
434
+ responses: {
435
+ "200": res("更新後の workspace"),
436
+ "403": res("権限不足"),
437
+ "404": res("workspace がこの org に属さない / 存在しない"),
438
+ "401": res("認証エラー"),
439
+ },
440
+ },
441
+ delete: {
442
+ summary: "workspace を削除する(セッション・persons・identities・API キーにカスケード)。owner / admin のみ。",
443
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
444
+ responses: {
445
+ "204": res("削除完了"),
446
+ "403": res("権限不足"),
447
+ "404": res("workspace がこの org に属さない"),
448
+ "401": res("認証エラー"),
449
+ },
450
+ },
451
+ }),
452
+ "/v1/orgs/{id}/workspaces/{wsId}/keys": tagged("Orgs (self-serve)", {
453
+ get: {
454
+ summary: "workspace の API キー一覧(owner / admin のみ)。",
455
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
456
+ responses: {
457
+ "200": res("キー一覧"),
458
+ "403": res("権限不足"),
459
+ "404": res("workspace がこの org に属さない"),
460
+ "401": res("認証エラー"),
461
+ },
462
+ },
463
+ post: {
464
+ summary: "新しい API キーを発行する(owner / admin のみ)。",
465
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
466
+ requestBody: jsonBody("IssueKey", false),
467
+ responses: {
468
+ "200": res("発行されたキー(raw は一度のみ返却)"),
469
+ "403": res("権限不足"),
470
+ "404": res("workspace がこの org に属さない"),
471
+ "401": res("認証エラー"),
472
+ },
473
+ },
474
+ }),
475
+ "/v1/orgs/{id}/workspaces/{wsId}/keys/{keyId}": tagged("Orgs (self-serve)", {
476
+ delete: {
477
+ summary: "API キーを無効化する(owner / admin のみ)。",
478
+ parameters: [
479
+ pathParam("id", "org id。"),
480
+ pathParam("wsId", "workspace id。"),
481
+ pathParam("keyId", "key id。"),
482
+ ],
483
+ responses: {
484
+ "204": res("無効化完了"),
485
+ "403": res("権限不足"),
486
+ "404": res("workspace / key がこの org に属さない、または存在しない"),
487
+ "401": res("認証エラー"),
488
+ },
489
+ },
490
+ }),
491
+ // ------------------------------------------------------------------
492
+ // Admin org surface (Wave G1). Platform admin token.
493
+ // ------------------------------------------------------------------
494
+ "/admin/v1/orgs": tagged("Orgs (admin)", {
495
+ post: {
496
+ summary: "org を作成する。",
497
+ security: adminAuth,
498
+ requestBody: jsonBody("CreateOrg"),
499
+ responses: {
500
+ "200": res("作成された org"),
501
+ "400": res("body が不正"),
502
+ "401": res("認証エラー"),
503
+ },
504
+ },
505
+ get: {
506
+ summary: "全 org を一覧取得する。",
507
+ security: adminAuth,
508
+ responses: {
509
+ "200": res("org 一覧"),
510
+ "401": res("認証エラー"),
511
+ },
512
+ },
513
+ }),
514
+ "/admin/v1/orgs/{id}": tagged("Orgs (admin)", {
515
+ get: {
516
+ summary: "単一の org を取得する。",
517
+ security: adminAuth,
518
+ parameters: [pathParam("id", "org id。")],
519
+ responses: {
520
+ "200": res("org"),
521
+ "404": res("org が見つからない"),
522
+ "401": res("認証エラー"),
523
+ },
524
+ },
525
+ delete: {
526
+ summary: "org を削除する(member と workspace にカスケード)。",
527
+ security: adminAuth,
528
+ parameters: [pathParam("id", "org id。")],
529
+ responses: {
530
+ "204": res("削除完了"),
531
+ "404": res("org が見つからない"),
532
+ "401": res("認証エラー"),
533
+ },
534
+ },
535
+ }),
536
+ "/admin/v1/orgs/{id}/members": tagged("Orgs (admin)", {
537
+ get: {
538
+ summary: "org の member 一覧。",
539
+ security: adminAuth,
540
+ parameters: [pathParam("id", "org id。")],
541
+ responses: {
542
+ "200": res("member 一覧"),
543
+ "404": res("org が見つからない"),
544
+ "401": res("認証エラー"),
545
+ },
546
+ },
547
+ post: {
548
+ summary: "member を追加する(email または userId)。",
549
+ security: adminAuth,
550
+ parameters: [pathParam("id", "org id。")],
551
+ requestBody: jsonBody("AddMember"),
552
+ responses: {
553
+ "200": res("追加された membership"),
554
+ "400": res("`userId` も `email` も無い、または body が不正"),
555
+ "404": res("org または email から user を解決できない"),
556
+ "401": res("認証エラー"),
557
+ },
558
+ },
559
+ }),
560
+ "/admin/v1/orgs/{id}/members/{userId}": tagged("Orgs (admin)", {
561
+ delete: {
562
+ summary: "member を削除する。最後の owner は削除拒否(`cannot_remove_last_owner`)。",
563
+ security: adminAuth,
564
+ parameters: [pathParam("id", "org id。"), pathParam("userId", "user id。")],
565
+ responses: {
566
+ "204": res("削除完了"),
567
+ "400": res("最後の owner は削除できない"),
568
+ "404": res("org が見つからない"),
569
+ "401": res("認証エラー"),
570
+ },
571
+ },
572
+ }),
573
+ "/admin/v1/orgs/{id}/workspaces": tagged("Orgs (admin)", {
574
+ post: {
575
+ summary: "org の下に workspace を作成し、(issueKey=false でなければ)初期 API キーも発行する。",
576
+ security: adminAuth,
577
+ parameters: [pathParam("id", "org id。")],
578
+ requestBody: jsonBody("CreateWorkspace"),
579
+ responses: {
580
+ "200": res("workspace(issueKey=false でない限り key も含む)"),
581
+ "400": res("body または workspace id が不正"),
582
+ "404": res("org が見つからない"),
583
+ "401": res("認証エラー"),
584
+ },
585
+ },
586
+ get: {
587
+ summary: "org の workspace 一覧。",
588
+ security: adminAuth,
589
+ parameters: [pathParam("id", "org id。")],
590
+ responses: {
591
+ "200": res("workspace 一覧"),
592
+ "404": res("org が見つからない"),
593
+ "401": res("認証エラー"),
594
+ },
595
+ },
596
+ }),
597
+ "/admin/v1/orgs/{id}/workspaces/{wsId}/keys": tagged("Orgs (admin)", {
598
+ get: {
599
+ summary: "org 配下の workspace の API キー一覧(ハッシュのみ)。",
600
+ security: adminAuth,
601
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
602
+ responses: {
603
+ "200": res("キー一覧"),
604
+ "404": res("workspace がこの org に属さない / org が無い"),
605
+ "401": res("認証エラー"),
606
+ },
607
+ },
608
+ post: {
609
+ summary: "org 配下の workspace に新しい API キーを発行する。`createWorkspaceUnderOrg` 後のキー rotation に使う。",
610
+ security: adminAuth,
611
+ parameters: [pathParam("id", "org id。"), pathParam("wsId", "workspace id。")],
612
+ requestBody: jsonBody("IssueKey", false),
613
+ responses: {
614
+ "200": res("発行されたキー(raw は一度のみ返却)"),
615
+ "400": res("リクエストボディが不正"),
616
+ "404": res("workspace がこの org に属さない / org が無い"),
617
+ "401": res("認証エラー"),
618
+ },
619
+ },
620
+ }),
621
+ "/admin/v1/orgs/{id}/workspaces/{wsId}/keys/{keyId}": tagged("Orgs (admin)", {
622
+ delete: {
623
+ summary: "org 配下の workspace の API キーを無効化する。",
624
+ security: adminAuth,
625
+ parameters: [
626
+ pathParam("id", "org id。"),
627
+ pathParam("wsId", "workspace id。"),
628
+ pathParam("keyId", "key id。"),
629
+ ],
630
+ responses: {
631
+ "204": res("無効化完了"),
632
+ "404": res("workspace / key がこの org に属さない、または存在しない"),
633
+ "401": res("認証エラー"),
634
+ },
635
+ },
636
+ }),
307
637
  "/admin/v1/workspaces": tagged("Workspaces (admin)", {
308
638
  post: {
309
639
  summary: "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
@@ -425,15 +755,22 @@ export function buildOpenApiDocument() {
425
755
  },
426
756
  schemas: {
427
757
  SessionInit: toComponent(sessionInitSchema),
758
+ SessionUpdate: toComponent(sessionUpdateSchema),
428
759
  // `EventBatch` inlines the per-event shape. A standalone `SessionEvent`
429
760
  // component (cross-`$ref`'d) is a planned follow-up alongside response
430
761
  // schemas — zod's `toJSONSchema` inlines by default.
431
762
  EventBatch: toComponent(eventBatchSchema),
432
763
  PersonCreate: toComponent(personCreateSchema),
433
764
  PersonUpdate: toComponent(personUpdateSchema),
765
+ AliasUpsert: toComponent(aliasUpsertSchema),
766
+ IdentityUpsert: toComponent(identityUpsertSchema),
434
767
  SearchRequest: toComponent(searchRequestSchema),
435
768
  CreateWorkspace: toComponent(createWorkspaceSchema),
436
769
  IssueKey: toComponent(issueKeySchema),
770
+ CreateOrg: toComponent(createOrgSchema),
771
+ OrgPatch: toComponent(orgPatchSchema),
772
+ WorkspacePatch: toComponent(workspacePatchSchema),
773
+ AddMember: toComponent(addMemberSchema),
437
774
  },
438
775
  },
439
776
  paths: buildPaths(),
@@ -29,6 +29,10 @@ export interface OrgStore {
29
29
  }): Promise<OrgRow>;
30
30
  getOrg(id: string): Promise<OrgRow | null>;
31
31
  listOrgs(): Promise<OrgRow[]>;
32
+ updateOrg(id: string, patch: {
33
+ name?: string;
34
+ metadata?: Record<string, unknown>;
35
+ }): Promise<OrgRow>;
32
36
  deleteOrg(id: string): Promise<void>;
33
37
  listMembers(orgId: string): Promise<OrgMembershipRow[]>;
34
38
  upsertMember(input: {
@@ -63,4 +67,7 @@ export interface OrgStore {
63
67
  }[]>;
64
68
  /** Returns true if the user is an org-member of the workspace's org. */
65
69
  userCanAccessWorkspace(userId: string, workspaceId: string): Promise<boolean>;
70
+ /** Returns true if the workspace's `org_id` matches. Admin-side check
71
+ * with no user context. */
72
+ workspaceInOrg(orgId: string, workspaceId: string): Promise<boolean>;
66
73
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cookie-auth org self-service routes (Wave G5).
3
+ *
4
+ * Auth gating only — the underlying logic lives in services/orgs.ts so
5
+ * the admin-token surface (`/admin/v1/orgs/*`) can call the same code
6
+ * without duplicating it.
7
+ *
8
+ * Mount BEFORE the workspace gate in src/server.ts; these endpoints
9
+ * have their own membership check and a chosen workspace would be
10
+ * irrelevant noise.
11
+ */
12
+ import { Hono } from "hono";
13
+ import type { EngramAuth } from "../auth";
14
+ import type { KeyStore } from "../key-store";
15
+ import type { OrgStore } from "../org-store";
16
+ export interface OrgsRouteOptions {
17
+ authHandler: EngramAuth;
18
+ orgStore: OrgStore;
19
+ keyStore: KeyStore;
20
+ }
21
+ type Env = {
22
+ Variables: {
23
+ request_id: string;
24
+ };
25
+ };
26
+ export declare function orgsRoutes(opts: OrgsRouteOptions): Hono<Env>;
27
+ export {};
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Cookie-auth org self-service routes (Wave G5).
3
+ *
4
+ * Auth gating only — the underlying logic lives in services/orgs.ts so
5
+ * the admin-token surface (`/admin/v1/orgs/*`) can call the same code
6
+ * without duplicating it.
7
+ *
8
+ * Mount BEFORE the workspace gate in src/server.ts; these endpoints
9
+ * have their own membership check and a chosen workspace would be
10
+ * irrelevant noise.
11
+ */
12
+ import { Hono } from "hono";
13
+ import { addMember, createWorkspaceUnderOrg, OrgServiceError, removeMember, revokeWorkspaceKey, updateOrg, updateOrgWorkspace, } from "../services/orgs";
14
+ const WRITE_ROLES = new Set(["owner", "admin"]);
15
+ export function orgsRoutes(opts) {
16
+ const app = new Hono();
17
+ const deps = { orgStore: opts.orgStore, keyStore: opts.keyStore };
18
+ const requireMember = async (req, orgId, minRole = "member") => {
19
+ const s = await opts.authHandler.api
20
+ .getSession({ headers: req.headers })
21
+ .catch(() => null);
22
+ if (!s?.user)
23
+ return jsonResponse(401, "unauthorized");
24
+ const memberships = await opts.orgStore.listOrgsForUser(s.user.id);
25
+ const here = memberships.find((m) => m.orgId === orgId);
26
+ if (!here)
27
+ return jsonResponse(403, "forbidden");
28
+ if (minRole !== "member" && !WRITE_ROLES.has(here.role)) {
29
+ return jsonResponse(403, "forbidden");
30
+ }
31
+ return { user: { id: s.user.id }, role: here.role };
32
+ };
33
+ // ---------- org ---------------------------------------------
34
+ app.get("/v1/orgs/:id", async (c) => {
35
+ const gate = await requireMember(c.req.raw, c.req.param("id"));
36
+ if (gate instanceof Response)
37
+ return gate;
38
+ const org = await opts.orgStore.getOrg(c.req.param("id"));
39
+ if (!org)
40
+ return c.json({ error: "org_not_found" }, 404);
41
+ return c.json({ org, role: gate.role });
42
+ });
43
+ app.patch("/v1/orgs/:id", async (c) => {
44
+ const id = c.req.param("id");
45
+ const gate = await requireMember(c.req.raw, id, "admin");
46
+ if (gate instanceof Response)
47
+ return gate;
48
+ return runService(c, async () => {
49
+ const body = (await c.req.json().catch(() => ({})));
50
+ const org = await updateOrg(deps, id, body);
51
+ return c.json({ org });
52
+ });
53
+ });
54
+ // ---------- members -----------------------------------------
55
+ app.get("/v1/orgs/:id/members", async (c) => {
56
+ const gate = await requireMember(c.req.raw, c.req.param("id"));
57
+ if (gate instanceof Response)
58
+ return gate;
59
+ return c.json({ members: await opts.orgStore.listMembers(c.req.param("id")) });
60
+ });
61
+ app.post("/v1/orgs/:id/members", async (c) => {
62
+ const orgId = c.req.param("id");
63
+ const gate = await requireMember(c.req.raw, orgId, "admin");
64
+ if (gate instanceof Response)
65
+ return gate;
66
+ return runService(c, async () => {
67
+ const body = (await c.req.json().catch(() => ({})));
68
+ const member = await addMember(deps, orgId, body);
69
+ return c.json({ member });
70
+ });
71
+ });
72
+ app.delete("/v1/orgs/:id/members/:userId", async (c) => {
73
+ const orgId = c.req.param("id");
74
+ const gate = await requireMember(c.req.raw, orgId, "admin");
75
+ if (gate instanceof Response)
76
+ return gate;
77
+ return runService(c, async () => {
78
+ await removeMember(deps, orgId, c.req.param("userId"));
79
+ return c.body(null, 204);
80
+ });
81
+ });
82
+ // ---------- workspaces -------------------------------------
83
+ app.get("/v1/orgs/:id/workspaces", async (c) => {
84
+ const gate = await requireMember(c.req.raw, c.req.param("id"));
85
+ if (gate instanceof Response)
86
+ return gate;
87
+ const workspaces = await opts.orgStore.listWorkspacesForOrg(c.req.param("id"));
88
+ return c.json({ workspaces });
89
+ });
90
+ app.patch("/v1/orgs/:id/workspaces/:wsId", async (c) => {
91
+ const orgId = c.req.param("id");
92
+ const gate = await requireMember(c.req.raw, orgId, "admin");
93
+ if (gate instanceof Response)
94
+ return gate;
95
+ const wsId = c.req.param("wsId");
96
+ const ok = await opts.orgStore.userCanAccessWorkspace(gate.user.id, wsId);
97
+ if (!ok)
98
+ return c.json({ error: "workspace_not_in_org" }, 404);
99
+ return runService(c, async () => {
100
+ const body = (await c.req.json().catch(() => ({})));
101
+ return c.json(await updateOrgWorkspace(deps, orgId, wsId, body));
102
+ });
103
+ });
104
+ app.delete("/v1/orgs/:id/workspaces/:wsId", async (c) => {
105
+ const orgId = c.req.param("id");
106
+ const gate = await requireMember(c.req.raw, orgId, "admin");
107
+ if (gate instanceof Response)
108
+ return gate;
109
+ const wsId = c.req.param("wsId");
110
+ const ok = await opts.orgStore.userCanAccessWorkspace(gate.user.id, wsId);
111
+ if (!ok)
112
+ return c.json({ error: "workspace_not_in_org" }, 404);
113
+ await opts.keyStore.deleteWorkspace(wsId);
114
+ return c.body(null, 204);
115
+ });
116
+ app.post("/v1/orgs/:id/workspaces", async (c) => {
117
+ const orgId = c.req.param("id");
118
+ const gate = await requireMember(c.req.raw, orgId, "admin");
119
+ if (gate instanceof Response)
120
+ return gate;
121
+ return runService(c, async () => {
122
+ const body = (await c.req.json().catch(() => ({})));
123
+ return c.json(await createWorkspaceUnderOrg(deps, orgId, body));
124
+ });
125
+ });
126
+ // ---------- api keys (scoped to a workspace) ----------------
127
+ app.get("/v1/orgs/:id/workspaces/:wsId/keys", async (c) => {
128
+ const orgId = c.req.param("id");
129
+ const gate = await requireMember(c.req.raw, orgId, "admin");
130
+ if (gate instanceof Response)
131
+ return gate;
132
+ const wsId = c.req.param("wsId");
133
+ const ok = await opts.orgStore.userCanAccessWorkspace(gate.user.id, wsId);
134
+ if (!ok)
135
+ return c.json({ error: "workspace_not_in_org" }, 404);
136
+ return c.json({ keys: await opts.keyStore.listKeys(wsId) });
137
+ });
138
+ app.post("/v1/orgs/:id/workspaces/:wsId/keys", async (c) => {
139
+ const orgId = c.req.param("id");
140
+ const gate = await requireMember(c.req.raw, orgId, "admin");
141
+ if (gate instanceof Response)
142
+ return gate;
143
+ const wsId = c.req.param("wsId");
144
+ const ok = await opts.orgStore.userCanAccessWorkspace(gate.user.id, wsId);
145
+ if (!ok)
146
+ return c.json({ error: "workspace_not_in_org" }, 404);
147
+ const body = (await c.req.json().catch(() => ({})));
148
+ const key = await opts.keyStore.issueKey(wsId, {
149
+ ...(body.name !== undefined ? { name: body.name } : {}),
150
+ });
151
+ return c.json(key);
152
+ });
153
+ app.delete("/v1/orgs/:id/workspaces/:wsId/keys/:keyId", async (c) => {
154
+ const orgId = c.req.param("id");
155
+ const gate = await requireMember(c.req.raw, orgId, "admin");
156
+ if (gate instanceof Response)
157
+ return gate;
158
+ const wsId = c.req.param("wsId");
159
+ const ok = await opts.orgStore.userCanAccessWorkspace(gate.user.id, wsId);
160
+ if (!ok)
161
+ return c.json({ error: "workspace_not_in_org" }, 404);
162
+ return runService(c, async () => {
163
+ await revokeWorkspaceKey(deps, wsId, c.req.param("keyId"));
164
+ return c.body(null, 204);
165
+ });
166
+ });
167
+ return app;
168
+ }
169
+ function jsonResponse(status, error) {
170
+ return new Response(JSON.stringify({ error }), {
171
+ status,
172
+ headers: { "content-type": "application/json" },
173
+ });
174
+ }
175
+ /** Run a service call, mapping OrgServiceError to a JSON response. */
176
+ async function runService(c, fn) {
177
+ try {
178
+ return await fn();
179
+ }
180
+ catch (e) {
181
+ if (e instanceof OrgServiceError)
182
+ return c.json({ error: e.code }, e.status);
183
+ throw e;
184
+ }
185
+ }
package/dist/schemas.d.ts CHANGED
@@ -161,6 +161,24 @@ export declare const createWorkspaceSchema: z.ZodObject<{
161
161
  export declare const issueKeySchema: z.ZodObject<{
162
162
  name: z.ZodOptional<z.ZodString>;
163
163
  }, z.core.$strip>;
164
+ export declare const createOrgSchema: z.ZodObject<{
165
+ id: z.ZodOptional<z.ZodString>;
166
+ name: z.ZodOptional<z.ZodString>;
167
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
168
+ }, z.core.$strip>;
169
+ export declare const orgPatchSchema: z.ZodObject<{
170
+ name: z.ZodOptional<z.ZodString>;
171
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
172
+ }, z.core.$strip>;
173
+ export declare const workspacePatchSchema: z.ZodObject<{
174
+ name: z.ZodOptional<z.ZodString>;
175
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
176
+ }, z.core.$strip>;
177
+ export declare const addMemberSchema: z.ZodObject<{
178
+ userId: z.ZodOptional<z.ZodString>;
179
+ email: z.ZodOptional<z.ZodString>;
180
+ role: z.ZodOptional<z.ZodString>;
181
+ }, z.core.$strip>;
164
182
  /**
165
183
  * Read and validate a JSON request body. Returns the parsed value, or a
166
184
  * `Response` (400) the caller should return as-is: