@k2b/cloud 0.31.0 → 0.33.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 (173) hide show
  1. package/package.json +8 -6
  2. package/src/_internal/define-app.ts +20 -9
  3. package/src/_internal/page-responses.ts +5 -1
  4. package/src/ai/app-skills.ts +1 -7
  5. package/src/ai/audio-tool.ts +2 -1
  6. package/src/ai/browser.ts +109 -0
  7. package/src/ai/capabilities.ts +6 -2
  8. package/src/ai/chart-block.ts +431 -81
  9. package/src/ai/chat/action-sentences.ts +13 -7
  10. package/src/ai/chat/blocks.tsx +11 -7
  11. package/src/ai/chat/builtin-tools.tsx +9 -7
  12. package/src/ai/chat/chart-block.tsx +218 -44
  13. package/src/ai/chat/explorer-block.tsx +276 -0
  14. package/src/ai/chat/file-tools.tsx +40 -1
  15. package/src/ai/chat/message-actions.tsx +6 -0
  16. package/src/ai/chat/message-utils.ts +7 -0
  17. package/src/ai/chat/messages.ts +16 -0
  18. package/src/ai/chat/presentation.tsx +13 -3
  19. package/src/ai/chat/reference-text.tsx +34 -0
  20. package/src/ai/chat/tool-groups.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +14 -0
  22. package/src/ai/chat/turn-view.tsx +17 -7
  23. package/src/ai/chat/view-block.browser-harness.tsx +35 -0
  24. package/src/ai/chat/view-block.tsx +417 -0
  25. package/src/ai/chat/visual-tools.tsx +32 -53
  26. package/src/ai/client-tool-results.ts +9 -0
  27. package/src/ai/code-source-contracts.ts +17 -2
  28. package/src/ai/code-source-tools.ts +1 -0
  29. package/src/ai/default-tool-contracts.ts +334 -91
  30. package/src/ai/default-tools.ts +89 -13
  31. package/src/ai/executor.ts +173 -31
  32. package/src/ai/file-error.ts +2 -0
  33. package/src/ai/file-tools.ts +27 -7
  34. package/src/ai/files-store.ts +60 -0
  35. package/src/ai/html-pdf-tool.ts +3 -2
  36. package/src/ai/http.ts +37 -12
  37. package/src/ai/index.ts +90 -0
  38. package/src/ai/local-tool-contracts.ts +244 -0
  39. package/src/ai/malformed-tool-call-repair.ts +144 -0
  40. package/src/ai/markdown-pdf-tool.ts +2 -1
  41. package/src/ai/memories.ts +48 -1
  42. package/src/ai/memory-learning.ts +17 -6
  43. package/src/ai/memory-tool.ts +1 -0
  44. package/src/ai/migrate.ts +15 -0
  45. package/src/ai/openai-api-messages.ts +53 -0
  46. package/src/ai/openai-api-slots.ts +40 -0
  47. package/src/ai/openai-api.ts +383 -0
  48. package/src/ai/protocol.ts +6 -2
  49. package/src/ai/provider.ts +17 -4
  50. package/src/ai/request-context.ts +33 -0
  51. package/src/ai/routes.ts +96 -57
  52. package/src/ai/runtime.ts +48 -12
  53. package/src/ai/settings.ts +5 -0
  54. package/src/ai/skill-catalog.ts +20 -5
  55. package/src/ai/skill-format.ts +17 -7
  56. package/src/ai/skill-tool.ts +15 -5
  57. package/src/ai/skills-routes.ts +20 -6
  58. package/src/ai/skills.ts +26 -7
  59. package/src/ai/store.ts +25 -0
  60. package/src/ai/system-prompt.ts +69 -10
  61. package/src/ai/timeline.ts +3 -1
  62. package/src/ai/tools.ts +3 -0
  63. package/src/ai/types.ts +9 -0
  64. package/src/ai/view-block.ts +941 -0
  65. package/src/ai/vision-tool.ts +2 -1
  66. package/src/api/admin-identity.ts +6 -1
  67. package/src/api/app-approval-actions.ts +36 -0
  68. package/src/api/app-approval.ts +10 -2
  69. package/src/api/approvals-loader.ts +15 -0
  70. package/src/api/approvals.ts +120 -0
  71. package/src/api/index.ts +3 -0
  72. package/src/api/me.ts +12 -3
  73. package/src/approvals/Approvals.browser-harness.tsx +16 -0
  74. package/src/approvals/Approvals.tsx +621 -0
  75. package/src/approvals/ApprovalsNavStatus.tsx +20 -0
  76. package/src/approvals/messages.ts +95 -0
  77. package/src/approvals/ui.ts +8 -0
  78. package/src/browser/app-approval.ts +32 -0
  79. package/src/browser/choose-files.tsx +26 -13
  80. package/src/browser/commands.ts +13 -4
  81. package/src/browser/file-provider-error.ts +11 -0
  82. package/src/browser/file-providers.ts +3 -12
  83. package/src/browser/files.tsx +1 -2
  84. package/src/browser/live-connection.ts +2 -3
  85. package/src/browser/navigation-search.ts +4 -1
  86. package/src/browser/provider-list.ts +2 -1
  87. package/src/browser/save-files.tsx +13 -2
  88. package/src/cli/account.ts +21 -2
  89. package/src/cli/commands.ts +4 -2
  90. package/src/contracts/app-approval.ts +120 -0
  91. package/src/contracts/approval-paths.ts +4 -0
  92. package/src/contracts/approvals.ts +81 -0
  93. package/src/contracts/command-path.ts +17 -0
  94. package/src/contracts/commands.ts +5 -15
  95. package/src/contracts/shared.ts +5 -0
  96. package/src/events/live-protocol.ts +2 -1
  97. package/src/events/live-server-message.ts +40 -0
  98. package/src/events/testing.ts +88 -0
  99. package/src/server/index.ts +2 -0
  100. package/src/server/middleware/auth.ts +11 -1
  101. package/src/server/request-timeout.ts +22 -0
  102. package/src/server/services/access.ts +4 -3
  103. package/src/services/app-approval.ts +103 -15
  104. package/src/services/approvals/cursor.ts +15 -0
  105. package/src/services/approvals/device.ts +112 -0
  106. package/src/services/approvals/index.ts +27 -11
  107. package/src/services/approvals/list.ts +120 -0
  108. package/src/services/approvals/migrate.ts +9 -1
  109. package/src/services/identity/models-only-auth.ts +25 -0
  110. package/src/services/notifications/email-frame.ts +16 -3
  111. package/src/services/outbox.ts +13 -6
  112. package/src/services/service-account-credentials.ts +22 -10
  113. package/src/services/settings/core-settings.ts +27 -0
  114. package/src/shared/ai-platform-prompt.ts +29 -3
  115. package/src/shared/email-html.ts +4 -0
  116. package/src/shared/index.ts +6 -1
  117. package/src/shared/markdown/extensions/code.ts +14 -0
  118. package/src/shared/markdown/index.ts +4 -4
  119. package/src/shared/theme.ts +41 -20
  120. package/src/ssr/AnnouncementBanners.tsx +30 -0
  121. package/src/ssr/AppBadge.tsx +4 -4
  122. package/src/ssr/AppLaunchpadPanel.tsx +6 -6
  123. package/src/ssr/CloudShell.island.tsx +91 -0
  124. package/src/ssr/GlobalSearchTrigger.tsx +51 -0
  125. package/src/ssr/Layout.tsx +56 -85
  126. package/src/ssr/LayoutHeader.tsx +31 -77
  127. package/src/ssr/LayoutHelp.tsx +2 -57
  128. package/src/ssr/LayoutHelpTrigger.tsx +51 -0
  129. package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
  130. package/src/ssr/LayoutRail.tsx +15 -69
  131. package/src/ssr/MinimalLayout.tsx +2 -3
  132. package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
  133. package/src/ssr/MobileNavigation.tsx +13 -8
  134. package/src/ssr/MobileProfileActions.tsx +10 -7
  135. package/src/ssr/PageTrailRow.tsx +14 -0
  136. package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
  137. package/src/ssr/PwaLayout.tsx +0 -2
  138. package/src/ssr/PwaRuntime.island.tsx +1 -1
  139. package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +2 -2
  140. package/src/ssr/ShellRuntime.island.tsx +170 -0
  141. package/src/ssr/TimezoneCookie.island.tsx +3 -19
  142. package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
  143. package/src/ssr/announcements-dialog.tsx +66 -0
  144. package/src/ssr/app-badges.ts +50 -6
  145. package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
  146. package/src/ssr/busy-action.ts +19 -0
  147. package/src/ssr/islands/index.ts +9 -6
  148. package/src/ssr/launchpad-surface.tsx +15 -0
  149. package/src/ssr/layout-help-registry.tsx +60 -0
  150. package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
  151. package/src/ssr/page-trail.browser-harness.tsx +52 -3
  152. package/src/ssr/preference-controller.ts +30 -21
  153. package/src/ssr/profile-actions.ts +26 -15
  154. package/src/ssr/profile-preferences-messages.ts +8 -4
  155. package/src/ssr/rail-context.ts +8 -12
  156. package/src/ssr/rail-navigation.ts +5 -3
  157. package/src/ssr/runtime.ts +6 -1
  158. package/src/ssr/shell-data.tsx +48 -0
  159. package/src/ssr/timezone-cookie.ts +22 -0
  160. package/src/ssr/workspace-navigation.ts +25 -2
  161. package/src/styles/approvals.css +390 -0
  162. package/src/styles/global.css +22 -7
  163. package/src/styles/utilities-navigation.css +13 -0
  164. package/src/workflows/language/compiler.ts +1 -1
  165. package/src/ssr/AppWorkspaceController.island.tsx +0 -21
  166. package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
  167. package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
  169. package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
  170. package/src/ssr/PageTrailRow.island.tsx +0 -20
  171. package/src/ssr/PageTrailRuntime.island.tsx +0 -26
  172. package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
  173. /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
+ import { aiMissingChatFileError } from "./file-error";
3
4
  import { aiProjectFilePathFromMount } from "./file-mount";
4
5
  import { aiFileStore, normalizeAiFilePath } from "./files-store";
5
6
  import { PdfPages, renderPdfPages } from "./pdf-render";
@@ -53,7 +54,7 @@ export const createCloudAiViewImageTool = (options: { resolveModel?: () => Promi
53
54
  if (snapshotRequired && !snapshot) throw new Error(`Attached image snapshot is unavailable: ${path}`);
54
55
  const stored =
55
56
  projectFile ?? snapshot ?? (projectPath === null ? await aiFileStore.read({ conversationId: ctx.conversationId, path }) : null);
56
- if (!stored) throw new Error(`No such file: ${path}`);
57
+ if (!stored) throw aiMissingChatFileError(path, ctx.localToolsEnabled);
57
58
  const pdf = stored.mediaType === "application/pdf";
58
59
  if (!pdf && !isAiImageMediaType(stored.mediaType)) throw new Error(`${path} is not a supported image or PDF (${stored.mediaType}).`);
59
60
  if (!pdf && input.pages !== undefined) throw new Error("pages can only be used with a PDF.");
@@ -5,6 +5,7 @@ import { describeRoute } from "hono-openapi";
5
5
  import { z } from "zod";
6
6
  import { listApps } from "../_internal/registry";
7
7
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
8
+ import { AI_INFERENCE_SCOPE } from "../contracts/shared";
8
9
  import { type AuthContext, auth, err, respond, v } from "../server";
9
10
  import { audit } from "../services/audit";
10
11
  import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
@@ -63,7 +64,11 @@ const ServiceAccountStatusInputSchema = z.object({ status: z.enum(["active", "di
63
64
  const ServiceAccountApiKeyInputSchema = z
64
65
  .object({
65
66
  name: z.string().trim().min(1).max(120),
66
- scopes: z.array(z.string().trim().min(1).max(64)).max(20).default([]),
67
+ scopes: z
68
+ .array(z.string().trim().min(1).max(64))
69
+ .max(20)
70
+ .default([])
71
+ .refine((scopes) => !scopes.includes(AI_INFERENCE_SCOPE), "ai:inference is reserved for models-only personal API keys"),
67
72
  expiresAt: z.iso.datetime().nullable().optional(),
68
73
  })
69
74
  .strict();
@@ -0,0 +1,36 @@
1
+ import { submitAiTurnAction } from "../ai/runtime";
2
+ import { aiConversations } from "../ai/store";
3
+ import { APP_ACTION_LIMITS } from "../contracts/app-approval";
4
+ import type { AppDeviceActions } from "../services/app-approval";
5
+ import { listPending } from "../services/approvals";
6
+ import { appActionSummary, appActionView } from "../services/approvals/device";
7
+
8
+ export const createDeviceActions = (appName: (appId: string, locale: string) => string): AppDeviceActions => {
9
+ const view: AppDeviceActions["view"] = async ({ userId, issuer, actionId }) => {
10
+ const { items } = await listPending({ approverUserId: userId, id: actionId, limit: 1 });
11
+ return items[0] ? appActionView(items[0], { issuer, appName }) : null;
12
+ };
13
+ return {
14
+ inbox: async ({ userId, issuer }) => {
15
+ const { items, count } = await listPending({ approverUserId: userId, limit: APP_ACTION_LIMITS.inboxActions });
16
+ const views = await Promise.all(items.map((row) => appActionView(row, { issuer, appName })));
17
+ return { actions: views.map(appActionSummary), moreActions: Math.max(0, count - items.length) };
18
+ },
19
+ view,
20
+ decide: async ({ userId, deviceId, issuer, actionId, digest, approved }) => {
21
+ const current = await view({ userId, issuer, actionId });
22
+ if (!current || current.digest !== digest) return false;
23
+ const pending = await aiConversations.findPendingTurnActionByApproval({ approvalId: actionId, ownerUserId: userId });
24
+ if (!pending) return false;
25
+ const result = await submitAiTurnAction({
26
+ conversationId: pending.conversationId,
27
+ turnId: pending.turnId,
28
+ callId: pending.callId,
29
+ action: { type: "approval_response", approved },
30
+ decidedBy: { userId, via: "cloud_login", deviceId },
31
+ signedInSession: false,
32
+ });
33
+ return result.ok;
34
+ },
35
+ };
36
+ };
@@ -16,13 +16,15 @@ import {
16
16
  } from "../contracts/app-approval";
17
17
  import { type AuthContext, auth, rateLimit, v } from "../server";
18
18
  import { type AppApprovalActor, AppApprovalError, appApproval, type createAppApprovalService } from "../services/app-approval";
19
+ import { appNameIn } from "../ssr/runtime";
20
+ import { createDeviceActions } from "./app-approval-actions";
19
21
 
20
22
  const PairingIdSchema = z.object({ pairingId: z.string().uuid() }).strict();
21
23
  const DevicePageSchema = z.object({ after: z.string().uuid().optional() }).strict();
22
24
  type Service = ReturnType<typeof createAppApprovalService>;
23
25
 
24
26
  /** This is a separate credential surface, never a user API-key or OAuth scope. */
25
- export const createAppApprovalRoutes = (service: Service = appApproval) => {
27
+ export const createAppApprovalRoutes = (service: Service = appApproval, actions = createDeviceActions) => {
26
28
  const actor = async (c: Parameters<typeof auth.session.getToken>[0]): Promise<AppApprovalActor> => {
27
29
  // The web session (or an explicit bearer session), never the mobile app's session: a phone
28
30
  // that pairs sign-in devices would hand out authority that outlives its removal.
@@ -104,7 +106,13 @@ export const createAppApprovalRoutes = (service: Service = appApproval) => {
104
106
  "/device",
105
107
  describeRoute({ tags: ["App approval"], summary: "Execute a one-use signed device command" }),
106
108
  v("json", AppDeviceRequestSchema),
107
- async (c) => c.json(await service.deviceCommand(c.req.valid("json"))),
109
+ async (c) =>
110
+ c.json(
111
+ await service.deviceCommand(
112
+ c.req.valid("json"),
113
+ actions((appId, locale) => appNameIn(c, appId, locale, appId)),
114
+ ),
115
+ ),
108
116
  )
109
117
  .post(
110
118
  "/login/start",
@@ -0,0 +1,15 @@
1
+ import type { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { resolveAiChatRequestContext } from "../ai/request-context";
4
+ import type { ApprovalListQuery, ApprovalPage } from "../contracts/approvals";
5
+ import type { AuthContext } from "../server/middleware/auth";
6
+ import { listPending, presentPendingApproval } from "../services/approvals/list";
7
+ import { getLocalizedRuntimeContext } from "../ssr/runtime";
8
+
9
+ export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>, query: ApprovalListQuery): Promise<ApprovalPage> => {
10
+ const ctx = await resolveAiChatRequestContext(c);
11
+ if (ctx instanceof Response) throw new HTTPException(403, { res: ctx });
12
+ const page = await listPending({ approverUserId: ctx.ownerUserId, appId: query.app, link: query.link, cursor: query.cursor });
13
+ const apps = getLocalizedRuntimeContext(c).apps;
14
+ return { ...page, items: page.items.map((row) => presentPendingApproval(row, apps)) };
15
+ };
@@ -0,0 +1,120 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { describeRoute } from "hono-openapi";
4
+ import { z } from "zod";
5
+ import { toAiActionFailureResponse } from "../ai/http";
6
+ import { resolveAiChatRequestContext } from "../ai/request-context";
7
+ import { submitAiTurnAction } from "../ai/runtime";
8
+ import { aiConversations } from "../ai/store";
9
+ import {
10
+ APPROVAL_CLIENT_HEADER,
11
+ ApprovalCountsSchema,
12
+ ApprovalDecisionInputSchema,
13
+ ApprovalDecisionResultSchema,
14
+ ApprovalListQuerySchema,
15
+ ApprovalPageSchema,
16
+ } from "../contracts/approvals";
17
+ import { ErrorResponseSchema } from "../contracts/shared";
18
+ import { rateLimit } from "../server";
19
+ import { respond } from "../server/api";
20
+ import { type AuthContext, auth } from "../server/middleware/auth";
21
+ import { jsonResponse, requiresAuth } from "../server/middleware/openapi";
22
+ import { v } from "../server/middleware/validator";
23
+ import { ok } from "../server/services";
24
+ import { approvalDecisionChannel, countPending } from "../services/approvals";
25
+ import { decodeApprovalCursor } from "../services/approvals/cursor";
26
+ import { loadPendingApprovals } from "./approvals-loader";
27
+
28
+ export const approvalRoutes = new Hono<AuthContext>()
29
+ .use(rateLimit())
30
+ .use(auth.requireRole("authenticated"))
31
+ .get(
32
+ "/",
33
+ describeRoute({
34
+ tags: ["Approvals"],
35
+ summary: "List pending Action approvals",
36
+ ...requiresAuth,
37
+ responses: {
38
+ 200: jsonResponse(ApprovalPageSchema, "Pending approvals"),
39
+ 400: jsonResponse(ErrorResponseSchema, "Invalid query"),
40
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
41
+ 403: jsonResponse(ErrorResponseSchema, "User-backed actor required"),
42
+ },
43
+ }),
44
+ v(
45
+ "query",
46
+ ApprovalListQuerySchema.refine((query) => !query.cursor || decodeApprovalCursor(query.cursor) !== null, "Invalid approval cursor."),
47
+ ),
48
+ async (c) => respond(c, ok(await loadPendingApprovals(c, c.req.valid("query")))),
49
+ )
50
+ .get(
51
+ "/counts",
52
+ describeRoute({
53
+ tags: ["Approvals"],
54
+ summary: "Count pending Action approvals",
55
+ ...requiresAuth,
56
+ responses: {
57
+ 200: jsonResponse(ApprovalCountsSchema, "Approval counts"),
58
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
59
+ 403: jsonResponse(ErrorResponseSchema, "User-backed actor required"),
60
+ },
61
+ }),
62
+ async (c) => {
63
+ const ctx = await resolveAiChatRequestContext(c);
64
+ if (ctx instanceof Response) return ctx;
65
+ return respond(c, ok(await countPending({ approverUserId: ctx.ownerUserId })));
66
+ },
67
+ )
68
+ .post(
69
+ "/:id/decision",
70
+ bodyLimit({ maxSize: 1024 }),
71
+ describeRoute({
72
+ tags: ["Approvals"],
73
+ summary: "Decide an Action approval and continue its chat",
74
+ ...requiresAuth,
75
+ responses: {
76
+ 200: jsonResponse(ApprovalDecisionResultSchema, "Approval decision"),
77
+ 400: jsonResponse(ErrorResponseSchema, "Invalid decision"),
78
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
79
+ 403: jsonResponse(ErrorResponseSchema, "Decision refused"),
80
+ 404: jsonResponse(ErrorResponseSchema, "Open approval not found"),
81
+ 409: jsonResponse(ErrorResponseSchema, "Already decided differently"),
82
+ 413: jsonResponse(ErrorResponseSchema, "Request body too large"),
83
+ },
84
+ }),
85
+ v("param", z.object({ id: z.uuid() })),
86
+ v("json", ApprovalDecisionInputSchema),
87
+ async (c) => {
88
+ const ctx = await resolveAiChatRequestContext(c);
89
+ if (ctx instanceof Response) return ctx;
90
+ const pending = await aiConversations.findPendingTurnActionByApproval({
91
+ approvalId: c.req.valid("param").id,
92
+ ownerUserId: ctx.ownerUserId,
93
+ });
94
+ if (!pending)
95
+ return toAiActionFailureResponse(c, {
96
+ ok: false,
97
+ status: 404,
98
+ message: "This request has expired — the assistant already moved on.",
99
+ });
100
+ const decision = c.req.valid("json");
101
+ const result = await submitAiTurnAction({
102
+ conversationId: pending.conversationId,
103
+ turnId: pending.turnId,
104
+ callId: pending.callId,
105
+ action: { type: "approval_response", ...decision },
106
+ decidedBy: {
107
+ userId: ctx.ownerUserId,
108
+ via: approvalDecisionChannel({
109
+ credentialKind: c.get("credentialKind"),
110
+ actor: ctx.actor,
111
+ client: c.req.header(APPROVAL_CLIENT_HEADER),
112
+ }),
113
+ },
114
+ toolApprovalContext: ctx.toolApprovalContext,
115
+ signedInSession: ctx.signedInSession,
116
+ });
117
+ if (!result.ok) return toAiActionFailureResponse(c, result);
118
+ return respond(c, ok({ status: decision.approved ? ("approved" as const) : ("rejected" as const) }));
119
+ },
120
+ );
package/src/api/index.ts CHANGED
@@ -33,6 +33,7 @@ import adminOutgoingMailRoutes from "./admin-outgoing-mail";
33
33
  import adminRailRoutes from "./admin-rail";
34
34
  import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
35
35
  import { createAppApprovalRoutes } from "./app-approval";
36
+ import { approvalRoutes } from "./approvals";
36
37
  import { appDiscoveryRoutes } from "./apps";
37
38
  import { createAuthRoutes } from "./auth";
38
39
  import { createCapabilityRoutes } from "./capabilities";
@@ -44,6 +45,7 @@ import { createSearchRoutes } from "./search";
44
45
  import { createSyncOpsProxyRoutes } from "./sync-ops";
45
46
  import { createWidgetRoutes } from "./widgets";
46
47
 
48
+ export { createAiOpenAiRoutes } from "../ai/openai-api";
47
49
  export { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
48
50
  export { cliPluginListRoutes } from "./cli-plugins";
49
51
  export { createPwaPhoneRoutes } from "./pwa-phone";
@@ -72,6 +74,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
72
74
  .route("/me", meRoutes)
73
75
  .route("/accounts", accountsEntitiesRoutes)
74
76
  .route("/apps", appDiscoveryRoutes)
77
+ .route("/approvals", approvalRoutes)
75
78
  .route("/announcements", announcementRoutes)
76
79
  .route("/admin/core/ai-usage", adminAiUsageRoutes)
77
80
  .route("/admin/core/ai-quotas", adminAiQuotaRoutes)
package/src/api/me.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * third-party management lives in the accounts admin app.
5
5
  */
6
6
 
7
- import { ok } from "@k2b/stdlib";
7
+ import { err, fail, ok } from "@k2b/stdlib";
8
8
  import { type Context, Hono, type MiddlewareHandler } from "hono";
9
9
  import { createMiddleware } from "hono/factory";
10
10
  import { describeRoute } from "hono-openapi";
@@ -51,6 +51,7 @@ import {
51
51
  webauthn,
52
52
  } from "../services";
53
53
  import { MandatePolicyV1Schema } from "../services/mandates";
54
+ import { coreSettings } from "../services/settings/api";
54
55
 
55
56
  import { createMeRailRoutes } from "./me-rail";
56
57
  import { createMeWebVitalsRoutes } from "./me-web-vitals";
@@ -614,13 +615,14 @@ const app = new Hono<AuthContext>()
614
615
  describeRoute({
615
616
  tags: ["Me"],
616
617
  summary: "Create current user API key",
617
- description: "Create a user-bound API key for the authenticated account. The raw token is returned once.",
618
+ description:
619
+ "Create a personal API key with full access (default) or models-only access to the OpenAI-compatible API when enabled. The raw token is returned once.",
618
620
  ...requiresAuth,
619
621
  responses: {
620
622
  201: jsonResponse(CreateUserApiKeyResponseSchema, "API key created"),
621
623
  400: jsonResponse(ErrorResponseSchema, "Failed to create API key"),
622
624
  401: jsonResponse(ErrorResponseSchema, "Authentication required"),
623
- 403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
625
+ 403: jsonResponse(ErrorResponseSchema, "Credential not permitted or models-only API turned off"),
624
626
  },
625
627
  }),
626
628
  v("json", CreateUserApiKeySchema),
@@ -630,7 +632,14 @@ const app = new Hono<AuthContext>()
630
632
  async () => {
631
633
  const user = c.get("user");
632
634
  const data = c.req.valid("json");
635
+ if (data.access === "models" && !(await coreSettings.get<boolean>("ai.openai_api_enabled")))
636
+ return fail(
637
+ err.forbidden(
638
+ "Models-only API keys are turned off. An administrator can turn on the OpenAI-compatible API in the AI settings.",
639
+ ),
640
+ );
633
641
  return serviceAccountCredentials.createUserApiToken({
642
+ access: data.access,
634
643
  user,
635
644
  name: data.name,
636
645
  expiresAt: data.expiresAt ?? null,
@@ -0,0 +1,16 @@
1
+ import { LocaleProvider } from "@k2b/ui";
2
+ import { render } from "solid-js/web";
3
+ import Approvals from "./Approvals";
4
+
5
+ /** Renders the real list for a browser test, which answers the approvals API with invented requests. */
6
+ const host = document.getElementById("root");
7
+ if (!host) throw new Error("Missing harness root");
8
+ const layout = new URLSearchParams(location.search).get("layout") === "band" ? "band" : "list";
9
+ render(
10
+ () => (
11
+ <LocaleProvider locale={document.documentElement.lang || "en"}>
12
+ <Approvals app="assistant" layout={layout} />
13
+ </LocaleProvider>
14
+ ),
15
+ host,
16
+ );