@k2b/cloud 0.31.0 → 0.32.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/package.json +6 -4
- package/src/_internal/define-app.ts +3 -1
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +11 -7
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +16 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +1 -0
- package/src/ai/chat/tool-names.ts +14 -0
- package/src/ai/chat/turn-view.tsx +17 -7
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +173 -31
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +90 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +7 -0
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +6 -2
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +96 -57
- package/src/ai/runtime.ts +48 -12
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +25 -0
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tools.ts +3 -0
- package/src/ai/types.ts +9 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +4 -2
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/shared.ts +5 -0
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +2 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +27 -11
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +9 -1
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/email-html.ts +4 -0
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/page-trail.browser-harness.tsx +52 -3
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/global.css +5 -2
package/src/ai/vision-tool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
};
|
package/src/api/app-approval.ts
CHANGED
|
@@ -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) =>
|
|
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:
|
|
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, "
|
|
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
|
+
);
|