@k2b/cloud 0.44.0 → 0.45.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 (73) hide show
  1. package/package.json +6 -2
  2. package/src/access/GroupCoverage.tsx +20 -5
  3. package/src/access/PermissionEditor.tsx +11 -2
  4. package/src/access/PrincipalAvatar.tsx +30 -21
  5. package/src/access/messages.ts +4 -4
  6. package/src/account/EntitySearch.tsx +12 -0
  7. package/src/ai/dictation-runtime.ts +6 -2
  8. package/src/ai/executor.ts +9 -3
  9. package/src/ai/index.ts +1 -0
  10. package/src/ai/project-tool.ts +4 -4
  11. package/src/ai/projects-authorization.ts +53 -0
  12. package/src/ai/projects-routes.ts +56 -46
  13. package/src/ai/projects.ts +100 -79
  14. package/src/ai/recent-resources.ts +3 -3
  15. package/src/ai/routes.ts +30 -30
  16. package/src/ai/skill-tool.ts +4 -4
  17. package/src/ai/skills-routes.ts +27 -26
  18. package/src/ai/skills.ts +120 -81
  19. package/src/api/accounts-entities.ts +6 -3
  20. package/src/api/admin-ai-skills.ts +4 -1
  21. package/src/api/auth.ts +9 -2
  22. package/src/browser/session.ts +39 -0
  23. package/src/browser/shared-text-history-validation.ts +151 -0
  24. package/src/browser/shared-text-history.ts +464 -0
  25. package/src/browser/shared-text-offline-authorized.ts +29 -0
  26. package/src/browser/shared-text-offline.ts +351 -0
  27. package/src/browser/shared-text-provider.ts +714 -0
  28. package/src/browser/shared-text-table-contract.ts +150 -0
  29. package/src/browser/shared-text.ts +19 -0
  30. package/src/contracts/shared.ts +2 -1
  31. package/src/server/api/respond.ts +10 -2
  32. package/src/server/index.ts +1 -0
  33. package/src/server/services/access.ts +23 -10
  34. package/src/server/services/index.ts +1 -0
  35. package/src/services/accounts/app.ts +30 -13
  36. package/src/services/accounts/entities.ts +29 -16
  37. package/src/services/accounts/group-sql.ts +23 -0
  38. package/src/services/accounts/groups.ts +16 -13
  39. package/src/services/accounts/identities.ts +8 -1
  40. package/src/services/accounts/identity-reconciliation.ts +3 -1
  41. package/src/services/accounts/local-groups.ts +69 -4
  42. package/src/services/accounts/users.ts +38 -8
  43. package/src/services/mandates/index.ts +2 -2
  44. package/src/services/mandates/policy.ts +1 -0
  45. package/src/services/session/index.ts +12 -3
  46. package/src/shared/email-html.ts +1 -0
  47. package/src/shared/markdown/extensions/katex.ts +8 -1
  48. package/src/shared/markdown/extensions/tables.ts +14 -1
  49. package/src/shared/markdown/index.ts +22 -11
  50. package/src/shared-text/anchors.ts +29 -0
  51. package/src/shared-text/awareness.ts +87 -0
  52. package/src/shared-text/changes.ts +27 -0
  53. package/src/shared-text/documents.ts +428 -77
  54. package/src/shared-text/events.ts +12 -2
  55. package/src/shared-text/index.ts +16 -1
  56. package/src/shared-text/log.ts +24 -5
  57. package/src/shared-text/snapshots.ts +69 -23
  58. package/src/shared-text/structural-history.ts +377 -0
  59. package/src/shared-text/table-actions.ts +432 -0
  60. package/src/shared-text/types.ts +24 -5
  61. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  62. package/src/ssr/CloudShell.island.tsx +19 -3
  63. package/src/ssr/Layout.tsx +1 -1
  64. package/src/ssr/MobileProfileActions.tsx +2 -2
  65. package/src/ssr/ProfilePreferences.tsx +2 -1
  66. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  67. package/src/ssr/admin-navigation.ts +1 -11
  68. package/src/ssr/app-launchpad.tsx +6 -1
  69. package/src/ssr/platform-messages.ts +2 -0
  70. package/src/ssr/profile-actions.ts +3 -6
  71. package/src/ssr/shell-data.tsx +1 -1
  72. package/src/workflows/language/expressions.ts +3 -2
  73. package/src/workflows/runtime/executor.ts +2 -1
package/src/ai/routes.ts CHANGED
@@ -8,7 +8,6 @@ import { ErrorResponseSchema } from "../contracts";
8
8
  import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
9
9
  import { capabilityActionPolicy } from "../contracts/capabilities";
10
10
  import {
11
- type AccessSubject,
12
11
  type AuthContext,
13
12
  auth,
14
13
  err,
@@ -80,6 +79,7 @@ import {
80
79
  } from "./message-queue";
81
80
  import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
82
81
  import { aiProjects } from "./projects";
82
+ import type { AiResourceContext } from "./projects-authorization";
83
83
  import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
84
84
  import { AiRecentResourceSchema, listAiRecentResources } from "./recent-resources";
85
85
  import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
@@ -224,6 +224,8 @@ const MemoryCreateSchema = z.object({
224
224
  const MemoryUpdateSchema = MemoryCreateSchema.partial().refine((value) => Object.keys(value).length > 0, "Pass a changed field.");
225
225
  const MemoryIdSchema = z.string().regex(AI_SHORT_ID_PATTERN);
226
226
 
227
+ const resourceContext = (c: Context<AuthContext>): AiResourceContext => ({ actor: c.get("actor"), accessSubject: c.get("accessSubject") });
228
+
227
229
  const notFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("Conversation")));
228
230
  const fileNotFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("File")));
229
231
 
@@ -232,14 +234,14 @@ const publicConversation = (conversation: AiConversation, projectId: string | nu
232
234
  id: conversation.shortId,
233
235
  projectId: conversation.projectId ? projectId : null,
234
236
  });
235
- const publicConversations = async (conversations: AiConversation[], subject: AccessSubject) => {
237
+ const publicConversations = async (conversations: AiConversation[], subject: AiResourceContext) => {
236
238
  const projectIds = conversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : []));
237
239
  const shortIds = await aiProjects.resolveShortIds(projectIds, subject);
238
240
  return conversations.map((conversation) =>
239
241
  publicConversation(conversation, conversation.projectId ? (shortIds.get(conversation.projectId) ?? null) : null),
240
242
  );
241
243
  };
242
- const publicConversationFor = async (conversation: AiConversation, subject: AccessSubject) =>
244
+ const publicConversationFor = async (conversation: AiConversation, subject: AiResourceContext) =>
243
245
  (await publicConversations([conversation], subject))[0]!;
244
246
  const publicMemories = async (userId: string, memories: Awaited<ReturnType<typeof aiMemories.list>>) => {
245
247
  const sourceIds = memories.flatMap((memory) => (memory.sourceConversationId ? [memory.sourceConversationId] : []));
@@ -260,7 +262,7 @@ const rememberLastUsedModel = (actor: RequestActor, model: import("../shared/ai-
260
262
  void aiUserPrefs.update(userId, { selection: model }).catch(() => undefined);
261
263
  };
262
264
 
263
- const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
265
+ const conversationDetail = async (conversation: AiConversation, subject: AiResourceContext) => {
264
266
  const [state, timeline, publicView] = await Promise.all([
265
267
  loadAiStreamState(conversation),
266
268
  aiConversations.listConversationTimeline({ conversationId: conversation.id }),
@@ -297,7 +299,7 @@ const prepareConversationForMessageRetry = async (conversationId: string): Promi
297
299
 
298
300
  const prepareLaunchDraft = async (
299
301
  body: z.infer<typeof AiCreateConversationInputSchema>,
300
- subject: AccessSubject | null,
302
+ subject: AiResourceContext | null,
301
303
  authority: import("../server").RequestAuthority | null = null,
302
304
  ) => {
303
305
  const content = [...(body.draft?.content ?? [])];
@@ -446,7 +448,7 @@ export const aiRoutes = (() => {
446
448
  }
447
449
  const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
448
450
  const availableSkills = toolsSupported
449
- ? (await aiSkills.list(c.get("accessSubject"), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
451
+ ? (await aiSkills.list(resourceContext(c), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
450
452
  : [];
451
453
  const skillCatalog = selectAiSkillCatalog(
452
454
  availableSkills.filter((skill) => !skill.manualOnly).map(({ name, description }) => ({ name, description })),
@@ -461,8 +463,8 @@ export const aiRoutes = (() => {
461
463
  })),
462
464
  ...(client?.localTools === "ask" ? [createCloudAiLocalBashTool(), ...createCloudAiLocalTools()] : []),
463
465
  ...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
464
- ...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"), appAccessAuthority(c))] : []),
465
- ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"), [], appAccessAuthority(c))] : []),
466
+ ...(availableSkills.length ? [createCloudAiLoadSkillTool(resourceContext(c), appAccessAuthority(c))] : []),
467
+ ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(resourceContext(c), [], appAccessAuthority(c))] : []),
466
468
  ]
467
469
  : [];
468
470
  const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
@@ -545,7 +547,7 @@ export const aiRoutes = (() => {
545
547
  const ctx = await resolveContext(c);
546
548
  if (ctx instanceof Response) return ctx;
547
549
  const query = c.req.valid("query");
548
- const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c.get("accessSubject"), "read") : null;
550
+ const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
549
551
  if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
550
552
  return respond(
551
553
  c,
@@ -561,7 +563,7 @@ export const aiRoutes = (() => {
561
563
  unassigned: query.unassigned,
562
564
  limit: query.limit,
563
565
  }),
564
- c.get("accessSubject"),
566
+ resourceContext(c),
565
567
  ),
566
568
  ),
567
569
  );
@@ -570,7 +572,7 @@ export const aiRoutes = (() => {
570
572
  const ctx = await resolveContext(c);
571
573
  if (ctx instanceof Response) return ctx;
572
574
  const query = c.req.valid("query");
573
- const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c.get("accessSubject"), "read") : null;
575
+ const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
574
576
  if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
575
577
  const page = await aiConversations.listConversationsPage({
576
578
  ownerUserId: ctx.ownerUserId,
@@ -583,15 +585,15 @@ export const aiRoutes = (() => {
583
585
  page: query.page ?? 1,
584
586
  perPage: query.perPage ?? 20,
585
587
  });
586
- return respond(c, ok({ ...page, items: await publicConversations(page.items, c.get("accessSubject")) }));
588
+ return respond(c, ok({ ...page, items: await publicConversations(page.items, resourceContext(c)) }));
587
589
  })
588
590
  .post("/conversations", v("json", AiCreateConversationInputSchema), async (c) => {
589
591
  const ctx = await resolveContext(c);
590
592
  if (ctx instanceof Response) return ctx;
591
593
  const body = c.req.valid("json");
592
- const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
594
+ const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
593
595
  if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
594
- const draft = await prepareLaunchDraft(body, c.get("accessSubject"), appAccessAuthority(c));
596
+ const draft = await prepareLaunchDraft(body, resourceContext(c), appAccessAuthority(c));
595
597
  if (!draft.ok) return respond(c, draft);
596
598
  const requestedCapabilityTools = (body.preloadTools ?? []).filter(
597
599
  (requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
@@ -642,9 +644,7 @@ export const aiRoutes = (() => {
642
644
  launchedByAppId: body.launchedByAppId,
643
645
  });
644
646
  // A replay may return a conversation assigned to a Project since creation.
645
- const currentProject = conversation.projectId
646
- ? await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read")
647
- : null;
647
+ const currentProject = conversation.projectId ? await aiProjects.get(conversation.projectId, resourceContext(c), "read") : null;
648
648
  if (conversation.projectId && !currentProject) return respond(c, fail(err.notFound("Project")));
649
649
  return respond(c, ok(publicConversation(conversation, currentProject?.shortId ?? null)), 201);
650
650
  } catch (error) {
@@ -659,7 +659,7 @@ export const aiRoutes = (() => {
659
659
  if (ctx instanceof Response) return ctx;
660
660
  const conversation = await loadConversation(c, ctx);
661
661
  if (!conversation) return notFound(c);
662
- return respond(c, ok(await conversationDetail(conversation, c.get("accessSubject"))));
662
+ return respond(c, ok(await conversationDetail(conversation, resourceContext(c))));
663
663
  })
664
664
  .get(
665
665
  "/conversations/:conversationId/submission/:draftRevision",
@@ -669,7 +669,7 @@ export const aiRoutes = (() => {
669
669
  if (ctx instanceof Response) return ctx;
670
670
  const conversation = await loadConversation(c, ctx);
671
671
  if (!conversation) return notFound(c);
672
- if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read"))) return notFound(c);
672
+ if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) return notFound(c);
673
673
  const submission = await aiConversations.getDraftSubmission({
674
674
  conversationId: conversation.id,
675
675
  ownerUserId: ctx.ownerUserId,
@@ -797,7 +797,7 @@ export const aiRoutes = (() => {
797
797
  pinned: body.pinned,
798
798
  });
799
799
  if (!updated) return notFound(c);
800
- return respond(c, ok(await publicConversationFor(updated, c.get("accessSubject"))));
800
+ return respond(c, ok(await publicConversationFor(updated, resourceContext(c))));
801
801
  })
802
802
  .put("/conversations/:conversationId/project", v("json", ConversationProjectInputSchema), async (c) => {
803
803
  const ctx = await resolveContext(c);
@@ -805,7 +805,7 @@ export const aiRoutes = (() => {
805
805
  const conversation = await loadConversation(c, ctx);
806
806
  if (!conversation) return notFound(c);
807
807
  const body = c.req.valid("json");
808
- const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
808
+ const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
809
809
  if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
810
810
  const result = await aiConversations.setConversationProject({
811
811
  conversationId: conversation.id,
@@ -820,7 +820,7 @@ export const aiRoutes = (() => {
820
820
  }
821
821
  return notFound(c);
822
822
  }
823
- return respond(c, ok(await publicConversationFor(result.conversation, c.get("accessSubject"))));
823
+ return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
824
824
  })
825
825
  .post("/conversations/:conversationId/pin", async (c) => {
826
826
  const ctx = await resolveContext(c);
@@ -832,7 +832,7 @@ export const aiRoutes = (() => {
832
832
  ownerUserId: ctx.ownerUserId,
833
833
  pinned: true,
834
834
  });
835
- return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
835
+ return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
836
836
  })
837
837
  .delete("/conversations/:conversationId/pin", async (c) => {
838
838
  const ctx = await resolveContext(c);
@@ -844,7 +844,7 @@ export const aiRoutes = (() => {
844
844
  ownerUserId: ctx.ownerUserId,
845
845
  pinned: false,
846
846
  });
847
- return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
847
+ return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
848
848
  })
849
849
  .put("/conversations/:conversationId/done", v("json", z.object({ done: z.boolean().nullable() })), async (c) => {
850
850
  const ctx = await resolveContext(c);
@@ -860,7 +860,7 @@ export const aiRoutes = (() => {
860
860
  return result.reason === "active_turn"
861
861
  ? respond(c, fail(err.conflict("Stop the current response before marking this chat done.")))
862
862
  : notFound(c);
863
- return respond(c, ok(await publicConversationFor(result.conversation, c.get("accessSubject"))));
863
+ return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
864
864
  })
865
865
  .post("/conversations/:conversationId/archive", async (c) => {
866
866
  const ctx = await resolveContext(c);
@@ -910,7 +910,7 @@ export const aiRoutes = (() => {
910
910
  error: error instanceof Error ? error.message : String(error),
911
911
  }),
912
912
  );
913
- return restored ? respond(c, ok(await publicConversationFor(restored, c.get("accessSubject")))) : notFound(c);
913
+ return restored ? respond(c, ok(await publicConversationFor(restored, resourceContext(c)))) : notFound(c);
914
914
  })
915
915
  .post("/conversations/:conversationId/viewed", async (c) => {
916
916
  const ctx = await resolveContext(c);
@@ -1007,7 +1007,7 @@ export const aiRoutes = (() => {
1007
1007
  };
1008
1008
  });
1009
1009
  const { input, message } = aiInputToUserMessage(aiTurnInputToContent({ content: turnContent }));
1010
- const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c.get("accessSubject")) : null;
1010
+ const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, resourceContext(c)) : null;
1011
1011
  if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
1012
1012
  try {
1013
1013
  const submission = {
@@ -1110,7 +1110,7 @@ export const aiRoutes = (() => {
1110
1110
  ? await aiConversations.getTurnRunConfig({ conversationId: conversation.id, turnId: target.loopId })
1111
1111
  : null;
1112
1112
  const originalProject = originalRunConfig?.kind !== "compact" ? originalRunConfig?.project : undefined;
1113
- const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, c.get("accessSubject")) : null;
1113
+ const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, resourceContext(c)) : null;
1114
1114
  if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
1115
1115
 
1116
1116
  try {
@@ -1189,7 +1189,7 @@ export const aiRoutes = (() => {
1189
1189
  }
1190
1190
 
1191
1191
  const body = c.req.valid("json");
1192
- if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read"))) {
1192
+ if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) {
1193
1193
  return respond(c, fail(err.notFound("Project")));
1194
1194
  }
1195
1195
  const forked = await aiConversations.forkConversation({
@@ -1198,7 +1198,7 @@ export const aiRoutes = (() => {
1198
1198
  ownerUserId: ctx.ownerUserId,
1199
1199
  title: body.title ?? conversation.title,
1200
1200
  });
1201
- return respond(c, ok(await conversationDetail(forked, c.get("accessSubject"))));
1201
+ return respond(c, ok(await conversationDetail(forked, resourceContext(c))));
1202
1202
  })
1203
1203
  .get("/conversations/:conversationId/enrichment", async (c) => {
1204
1204
  const ctx = await resolveContext(c);
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import type { AccessSubject } from "../server";
3
2
  import type { RequestAuthority } from "../server/middleware/auth";
4
3
  import { AI_SKILL_FILE_MOUNT } from "./file-mount";
4
+ import type { AiResourceContext } from "./projects-authorization";
5
5
  import { AI_SHORT_ID_PATTERN } from "./short-id";
6
6
  import { type AiSkillCatalogEntry, mergeAiSkillCatalog, rankAiSkillCatalog, searchAiSkillCatalog } from "./skill-catalog";
7
7
  import { AI_SKILL_NAME_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
@@ -12,7 +12,7 @@ import { defineAiTool } from "./tools";
12
12
  export async function loadSelectedAiSkills(
13
13
  ids: readonly string[],
14
14
  turnId: string,
15
- subject: AccessSubject | null,
15
+ subject: AiResourceContext | null,
16
16
  allowed: boolean,
17
17
  authority: RequestAuthority | null = null,
18
18
  ) {
@@ -58,7 +58,7 @@ export const CloudAiLoadSkillOutputSchema = z.object({
58
58
  files: z.array(z.string()),
59
59
  });
60
60
 
61
- export const createCloudAiLoadSkillTool = (subject: AccessSubject, authority: RequestAuthority | null = null) =>
61
+ export const createCloudAiLoadSkillTool = (subject: AiResourceContext, authority: RequestAuthority | null = null) =>
62
62
  defineAiTool({
63
63
  name: "load_skill",
64
64
  description:
@@ -92,7 +92,7 @@ export const CloudAiSearchSkillsOutputSchema = z.object({
92
92
  });
93
93
 
94
94
  export const createCloudAiSearchSkillsTool = (
95
- subject: AccessSubject | null,
95
+ subject: AiResourceContext | null,
96
96
  localSkills: readonly AiSkillCatalogEntry[] = [],
97
97
  authority: RequestAuthority | null = null,
98
98
  ) =>
@@ -1,4 +1,4 @@
1
- import { Hono, type MiddlewareHandler } from "hono";
1
+ import { type Context, Hono, type MiddlewareHandler } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import { z } from "zod";
4
4
  import { getApp } from "../_internal/registry";
@@ -10,6 +10,7 @@ import { appAccessAuthority, canUseApp } from "../services/app-access";
10
10
  import { recordAppAccessDenied } from "../services/app-access-metrics";
11
11
  import { localizedAppName } from "../ssr/runtime";
12
12
  import { aiProjects } from "./projects";
13
+ import { AiResourceCredentialError } from "./projects-authorization";
13
14
  import {
14
15
  AI_SKILL_DESCRIPTION_MAX_CHARS,
15
16
  AI_SKILL_EXTRA_FRONTMATTER_MAX_CHARS,
@@ -61,6 +62,11 @@ const publicSkill = (c: RouteContext, skill: AiSkill): AiSkill => ({ ...skill, i
61
62
  const isUniqueViolation = (error: unknown): boolean =>
62
63
  typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "23505";
63
64
 
65
+ const resourceSubject = (c: Context<AuthContext>) => ({
66
+ actor: c.get("actor"),
67
+ accessSubject: c.get("accessSubject"),
68
+ });
69
+
64
70
  type AiSkillsRouteDependencies = {
65
71
  limit?: MiddlewareHandler<AuthContext>;
66
72
  authenticate?: MiddlewareHandler<AuthContext>;
@@ -85,6 +91,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
85
91
  // The list combines skills from separate grants; the writer of one can rename it next to another skill's text.
86
92
  .use("*", noTransform())
87
93
  .onError((error, c) => {
94
+ if (error instanceof AiResourceCredentialError) return respond(c, fail(err.forbidden(error.message)));
88
95
  if (error instanceof AiSkillAppForbiddenError) return respond(c, fail(err.forbidden(error.message)));
89
96
  if (error instanceof HTTPException) return error.getResponse();
90
97
  throw error;
@@ -100,7 +107,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
100
107
  .refine(({ q, name }) => q === undefined || name === undefined, "Use either q or name, not both."),
101
108
  ),
102
109
  async (c) => {
103
- const subject = c.get("accessSubject") ?? null;
110
+ const subject = resourceSubject(c);
104
111
  const { q, name } = c.req.valid("query");
105
112
  const skills =
106
113
  name !== undefined
@@ -112,7 +119,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
112
119
  },
113
120
  )
114
121
  .get("/:skillId", async (c) => {
115
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "read", appAccessAuthority(c));
122
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "read", appAccessAuthority(c));
116
123
  return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
117
124
  })
118
125
  .use("*", requireCoreAccess)
@@ -128,7 +135,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
128
135
  }),
129
136
  ),
130
137
  async (c) => {
131
- const subject = c.get("accessSubject") ?? null,
138
+ const subject = resourceSubject(c),
132
139
  query = c.req.valid("query");
133
140
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
134
141
  const result = project
@@ -145,7 +152,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
145
152
  },
146
153
  )
147
154
  .get("/:skillId/projects", async (c) => {
148
- const subject = c.get("accessSubject") ?? null;
155
+ const subject = resourceSubject(c);
149
156
  const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c));
150
157
  const projects = skill ? await aiSkills.linkedProjects(skill.id, subject, appAccessAuthority(c)) : null;
151
158
  return projects
@@ -153,7 +160,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
153
160
  : respond(c, fail(err.notFound("Skill")));
154
161
  })
155
162
  .put("/:skillId/projects/:projectId", v("json", z.object({ linked: z.boolean() }).strict()), async (c) => {
156
- const subject = c.get("accessSubject") ?? null;
163
+ const subject = resourceSubject(c);
157
164
  const [skill, project] = await Promise.all([
158
165
  aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c)),
159
166
  aiProjects.getByShortId(c.req.param("projectId")!, subject, "admin"),
@@ -164,7 +171,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
164
171
  : respond(c, fail(err.notFound("Skill or Project")));
165
172
  })
166
173
  .post("/", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", SkillFieldsSchema), async (c) => {
167
- const subject = c.get("accessSubject");
174
+ const subject = resourceSubject(c);
168
175
  if (!subject) return respond(c, fail(err.forbidden("Skills require an authenticated access subject.")));
169
176
  try {
170
177
  return respond(
@@ -179,10 +186,10 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
179
186
  }
180
187
  })
181
188
  .put("/:skillId", v("json", UpdateSkillSchema), async (c) => {
182
- const existing = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "write", appAccessAuthority(c));
189
+ const existing = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "write", appAccessAuthority(c));
183
190
  if (!existing) return respond(c, fail(err.notFound("Skill")));
184
191
  try {
185
- const skill = await aiSkills.update(existing.id, c.get("accessSubject") ?? null, c.req.valid("json"), appAccessAuthority(c));
192
+ const skill = await aiSkills.update(existing.id, resourceSubject(c), c.req.valid("json"), appAccessAuthority(c));
186
193
  return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
187
194
  } catch (error) {
188
195
  if (isUniqueViolation(error)) return respond(c, fail(err.conflict("A skill with this name already exists.")));
@@ -192,37 +199,37 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
192
199
  }
193
200
  })
194
201
  .delete("/:skillId", async (c) => {
195
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
196
- return skill && (await aiSkills.delete(skill.id, c.get("accessSubject") ?? null, appAccessAuthority(c)))
202
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
203
+ return skill && (await aiSkills.delete(skill.id, resourceSubject(c), appAccessAuthority(c)))
197
204
  ? respond(c, ok({ deleted: true }))
198
205
  : respond(c, fail(err.notFound("Skill")));
199
206
  })
200
207
  .put("/:skillId/enabled", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", SkillEnabledSchema), async (c) => {
201
- const subject = c.get("accessSubject") ?? null;
208
+ const subject = resourceSubject(c);
202
209
  const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "read", appAccessAuthority(c));
203
210
  const enabled = skill ? await aiSkills.setEnabled(skill.id, subject, c.req.valid("json").enabled, appAccessAuthority(c)) : null;
204
211
  return enabled === null ? respond(c, fail(err.notFound("Skill"))) : respond(c, ok({ enabled }));
205
212
  })
206
213
  .get("/:skillId/access", async (c) => {
207
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
208
- const access = skill ? await aiSkills.listAccess(skill.id, c.get("accessSubject") ?? null, appAccessAuthority(c)) : null;
214
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
215
+ const access = skill ? await aiSkills.listAccess(skill.id, resourceSubject(c), appAccessAuthority(c)) : null;
209
216
  return access ? respond(c, ok({ access })) : respond(c, fail(err.notFound("Skill")));
210
217
  })
211
218
  .post("/:skillId/access", v("json", SkillAccessSchema), async (c) => {
212
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
219
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
213
220
  const access = skill
214
- ? await aiSkills.grantAccess(skill.id, c.get("accessSubject") ?? null, c.req.valid("json"), undefined, appAccessAuthority(c))
221
+ ? await aiSkills.grantAccess(skill.id, resourceSubject(c), c.req.valid("json"), undefined, appAccessAuthority(c))
215
222
  : null;
216
223
  return access ? respond(c, ok({ access }), 201) : respond(c, fail(err.notFound("Skill")));
217
224
  })
218
225
  .patch("/:skillId/access/:accessId", v("json", SkillAccessUpdateSchema), async (c) => {
219
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
226
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
220
227
  try {
221
228
  return skill &&
222
229
  (await aiSkills.updateAccess(
223
230
  skill.id,
224
231
  c.req.param("accessId")!,
225
- c.get("accessSubject") ?? null,
232
+ resourceSubject(c),
226
233
  c.req.valid("json").permission,
227
234
  undefined,
228
235
  appAccessAuthority(c),
@@ -235,16 +242,10 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
235
242
  }
236
243
  })
237
244
  .delete("/:skillId/access/:accessId", async (c) => {
238
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
245
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
239
246
  try {
240
247
  return skill &&
241
- (await aiSkills.revokeAccess(
242
- skill.id,
243
- c.req.param("accessId")!,
244
- c.get("accessSubject") ?? null,
245
- undefined,
246
- appAccessAuthority(c),
247
- ))
248
+ (await aiSkills.revokeAccess(skill.id, c.req.param("accessId")!, resourceSubject(c), undefined, appAccessAuthority(c)))
248
249
  ? respond(c, ok({ deleted: true }))
249
250
  : respond(c, fail(err.notFound("Access entry")));
250
251
  } catch (error) {