@k2b/cloud 0.41.0 → 0.42.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 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
package/src/ai/store.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { DoneReason, InboundEvent, LoopAggregate, Message, SessionStore, StoreEntry } from "@k2b/nessi";
|
|
2
2
|
import type { Usage } from "@k2b/nessi/ai";
|
|
3
3
|
import { type SQL, sql } from "bun";
|
|
4
|
-
import {
|
|
4
|
+
import { capabilityRequestHash } from "../capabilities/claims";
|
|
5
|
+
import { restoreReviewSnapshot, serializeReviewSnapshot } from "../capabilities/review-storage";
|
|
6
|
+
import { type CapabilityActionReviewPresentation, CapabilityActionReviewPresentationSchema } from "../contracts/capabilities";
|
|
5
7
|
import { logger } from "../services/logging";
|
|
6
8
|
import { toPgTextArray } from "../services/postgres";
|
|
7
9
|
import { AI_TOOL_CHECK_STALE_MS, type AiActionCheck, type AiStoredActionReview } from "./action-review";
|
|
@@ -14,6 +16,13 @@ import { aiTurnErrorText } from "./chat/turn-error";
|
|
|
14
16
|
import { AI_MEMORY_LEARNING_DEFAULT_ENABLED } from "./prefs";
|
|
15
17
|
import type { AiTurnBlock } from "./protocol";
|
|
16
18
|
import { collectRecentSentResourceRefs, RECENT_SENT_CONVERSATION_SCAN_LIMIT, RECENT_SENT_MESSAGE_SCAN_LIMIT } from "./recent-sent-refs";
|
|
19
|
+
import {
|
|
20
|
+
compactCapabilityReview,
|
|
21
|
+
compactReviewMapSql,
|
|
22
|
+
compactReviewMetaSql,
|
|
23
|
+
compactStoredReview,
|
|
24
|
+
storedReviewWithContent,
|
|
25
|
+
} from "./review-content";
|
|
17
26
|
import { AiRunTimeout } from "./run-timeout";
|
|
18
27
|
import { withAiShortId, withAiShortIdForDb } from "./short-id";
|
|
19
28
|
import { parseAiTodoPlan } from "./todo-contracts";
|
|
@@ -530,9 +539,9 @@ const parseJsonValue = <T>(value: unknown): T => {
|
|
|
530
539
|
return value as T;
|
|
531
540
|
};
|
|
532
541
|
|
|
533
|
-
const parseCapabilityActionReview = (value: unknown):
|
|
542
|
+
const parseCapabilityActionReview = (value: unknown): CapabilityActionReviewPresentation | undefined => {
|
|
534
543
|
if (value === null || value === undefined) return undefined;
|
|
535
|
-
const parsed =
|
|
544
|
+
const parsed = CapabilityActionReviewPresentationSchema.safeParse(restoreReviewSnapshot(parseJsonValue(value)));
|
|
536
545
|
return parsed.success ? parsed.data : undefined;
|
|
537
546
|
};
|
|
538
547
|
|
|
@@ -664,6 +673,14 @@ const intValue = (value: number | string | null | undefined): number => Math.max
|
|
|
664
673
|
|
|
665
674
|
const rowToMessage = (row: MessageRow): AiStoredMessage => {
|
|
666
675
|
const message = parseJsonValue<Message>(row.message);
|
|
676
|
+
const meta = row.meta ? parseJsonValue<AiStoredMessage["meta"]>(row.meta) : null;
|
|
677
|
+
if (meta?.toolReviews)
|
|
678
|
+
meta.toolReviews = Object.fromEntries(
|
|
679
|
+
Object.entries(meta.toolReviews).map(([callId, review]) => [
|
|
680
|
+
callId,
|
|
681
|
+
compactStoredReview(review, { conversationId: row.conversation_id, turnId: row.loop_id ?? "", callId }),
|
|
682
|
+
]),
|
|
683
|
+
);
|
|
667
684
|
return {
|
|
668
685
|
id: row.id,
|
|
669
686
|
shortId: row.short_id,
|
|
@@ -679,7 +696,7 @@ const rowToMessage = (row: MessageRow): AiStoredMessage => {
|
|
|
679
696
|
loopAggregate: row.loop_aggregate ? parseJsonValue<LoopAggregate>(row.loop_aggregate) : null,
|
|
680
697
|
loopDoneReason: row.loop_done_reason,
|
|
681
698
|
compactedAt: row.compacted_at ? iso(row.compacted_at) : null,
|
|
682
|
-
meta
|
|
699
|
+
meta,
|
|
683
700
|
feedback:
|
|
684
701
|
Number(row.feedback_rating) === 1 || Number(row.feedback_rating) === -1
|
|
685
702
|
? {
|
|
@@ -733,7 +750,16 @@ const pendingActionToPublicEvent = (row: PendingActionRow): AiPendingTurnAction
|
|
|
733
750
|
name: row.tool_name,
|
|
734
751
|
args: row.args,
|
|
735
752
|
message: row.message ?? undefined,
|
|
736
|
-
review:
|
|
753
|
+
review: (() => {
|
|
754
|
+
const review = parseCapabilityActionReview(row.review);
|
|
755
|
+
return review
|
|
756
|
+
? compactCapabilityReview(review, {
|
|
757
|
+
conversationId: row.conversation_id,
|
|
758
|
+
turnId: row.turn_id,
|
|
759
|
+
callId: row.kind === "custom_approval" ? row.call_id.replace(/-approval-[0-9]+$/, "") : row.call_id,
|
|
760
|
+
})
|
|
761
|
+
: undefined;
|
|
762
|
+
})(),
|
|
737
763
|
allowAlways: row.allow_always,
|
|
738
764
|
allowChat: row.allow_always || row.allow_chat === true,
|
|
739
765
|
...(row.remember_tool_name === AI_WEBSITE_APPROVAL_TOOL ? { website: row.approval_scope } : {}),
|
|
@@ -749,7 +775,16 @@ const rowToPendingActionRecord = (row: PendingActionRow): AiPendingTurnActionRec
|
|
|
749
775
|
name: row.tool_name,
|
|
750
776
|
args: row.args,
|
|
751
777
|
message: row.message ?? undefined,
|
|
752
|
-
review:
|
|
778
|
+
review: (() => {
|
|
779
|
+
const review = parseCapabilityActionReview(row.review);
|
|
780
|
+
return review
|
|
781
|
+
? compactCapabilityReview(review, {
|
|
782
|
+
conversationId: row.conversation_id,
|
|
783
|
+
turnId: row.turn_id,
|
|
784
|
+
callId: row.kind === "custom_approval" ? row.call_id.replace(/-approval-[0-9]+$/, "") : row.call_id,
|
|
785
|
+
})
|
|
786
|
+
: undefined;
|
|
787
|
+
})(),
|
|
753
788
|
approvalScope: row.approval_scope,
|
|
754
789
|
allowAlways: row.allow_always,
|
|
755
790
|
allowChat: row.allow_always || row.allow_chat === true,
|
|
@@ -994,7 +1029,7 @@ const recordAiToolMeta = async (
|
|
|
994
1029
|
UPDATE ${db(table)}
|
|
995
1030
|
SET meta = COALESCE(meta, '{}'::jsonb) || jsonb_build_object(
|
|
996
1031
|
${key}::text, COALESCE(meta->${key}::text, '{}'::jsonb) ||
|
|
997
|
-
jsonb_build_object(${input.callId}::text, ${JSON.stringify(value)}::text::jsonb)
|
|
1032
|
+
jsonb_build_object(${input.callId}::text, ${key === "toolReviews" ? serializeReviewSnapshot(value) : JSON.stringify(value)}::text::jsonb)
|
|
998
1033
|
)
|
|
999
1034
|
WHERE conversation_id = ${input.conversationId}::uuid AND loop_id = ${input.turnId}::text
|
|
1000
1035
|
AND role = 'assistant'
|
|
@@ -1005,8 +1040,16 @@ const recordAiToolMeta = async (
|
|
|
1005
1040
|
if (!written) throw new Error("AI tool call message is unavailable.");
|
|
1006
1041
|
};
|
|
1007
1042
|
|
|
1008
|
-
export const recordAiToolReview = (input: {
|
|
1009
|
-
|
|
1043
|
+
export const recordAiToolReview = async (input: {
|
|
1044
|
+
conversationId: string;
|
|
1045
|
+
turnId: string;
|
|
1046
|
+
callId: string;
|
|
1047
|
+
review: AiStoredActionReview;
|
|
1048
|
+
}): Promise<AiStoredActionReview> => {
|
|
1049
|
+
const stored = storedReviewWithContent(input.review, input);
|
|
1050
|
+
await recordAiToolMeta(input, "toolReviews", stored);
|
|
1051
|
+
return compactStoredReview(stored, input);
|
|
1052
|
+
};
|
|
1010
1053
|
|
|
1011
1054
|
export const recordAiToolCheck = (input: { conversationId: string; turnId: string; callId: string; check: AiActionCheck }, db = sql) =>
|
|
1012
1055
|
recordAiToolMeta(input, "toolChecks", input.check, db);
|
|
@@ -1319,11 +1362,45 @@ export const saveAiDraftInTransaction = async (
|
|
|
1319
1362
|
};
|
|
1320
1363
|
};
|
|
1321
1364
|
|
|
1365
|
+
export class AiConversationCreationError extends Error {
|
|
1366
|
+
constructor(readonly reason: "conflict" | "unavailable") {
|
|
1367
|
+
super(
|
|
1368
|
+
reason === "conflict"
|
|
1369
|
+
? "This creation request was already used with different input."
|
|
1370
|
+
: "The conversation from this request is no longer available.",
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1322
1375
|
export const aiConversations: AiConversationService = {
|
|
1376
|
+
getDraftSubmission: async ({ conversationId, ownerUserId, revision }) => {
|
|
1377
|
+
const [row] = await sql<{ message_id: string; turn_id: string | null }[]>`
|
|
1378
|
+
SELECT message.short_id AS message_id, turn.short_id AS turn_id
|
|
1379
|
+
FROM ai.messages message
|
|
1380
|
+
JOIN ai.conversations conversation ON conversation.id = message.conversation_id
|
|
1381
|
+
LEFT JOIN ai.turns turn ON turn.id::text = message.loop_id AND turn.conversation_id = conversation.id
|
|
1382
|
+
WHERE conversation.id = ${conversationId}::uuid AND conversation.created_by_user_id = ${ownerUserId}::uuid
|
|
1383
|
+
AND conversation.archived_at IS NULL AND message.role = 'user'
|
|
1384
|
+
AND message.meta->>'submittedDraftRevision' = ${String(revision)}
|
|
1385
|
+
LIMIT 1
|
|
1386
|
+
`;
|
|
1387
|
+
return row ? { messageId: row.message_id, turnId: row.turn_id } : null;
|
|
1388
|
+
},
|
|
1323
1389
|
createConversation: async (input) => {
|
|
1324
|
-
const
|
|
1325
|
-
"
|
|
1326
|
-
(
|
|
1390
|
+
const normalized = {
|
|
1391
|
+
title: input.title?.trim() || "New chat",
|
|
1392
|
+
description: input.description?.trim() ?? "",
|
|
1393
|
+
projectId: input.projectId ?? null,
|
|
1394
|
+
draft: input.draft ?? [],
|
|
1395
|
+
preloadTools: input.preloadTools ?? [],
|
|
1396
|
+
allowedTools: input.allowedTools ?? null,
|
|
1397
|
+
launchedByAppId: input.launchedByAppId?.trim() || null,
|
|
1398
|
+
};
|
|
1399
|
+
const insert = async (db: SQL) => {
|
|
1400
|
+
const rows = await withAiShortIdForDb(
|
|
1401
|
+
db,
|
|
1402
|
+
"idx_ai_conversations_short_id",
|
|
1403
|
+
(attempt, shortId) => attempt<ConversationRow[]>`
|
|
1327
1404
|
INSERT INTO ai.conversations (
|
|
1328
1405
|
short_id,
|
|
1329
1406
|
title,
|
|
@@ -1339,8 +1416,8 @@ export const aiConversations: AiConversationService = {
|
|
|
1339
1416
|
)
|
|
1340
1417
|
SELECT
|
|
1341
1418
|
${shortId},
|
|
1342
|
-
${
|
|
1343
|
-
${
|
|
1419
|
+
${normalized.title},
|
|
1420
|
+
${normalized.description},
|
|
1344
1421
|
${input.projectId ?? null}::uuid,
|
|
1345
1422
|
${JSON.stringify(input.draft ?? [])}::text::jsonb,
|
|
1346
1423
|
${input.draft?.length ? 1 : 0},
|
|
@@ -1352,10 +1429,38 @@ export const aiConversations: AiConversationService = {
|
|
|
1352
1429
|
WHERE ${input.projectId ?? null}::uuid IS NULL
|
|
1353
1430
|
OR EXISTS (SELECT 1 FROM ai.projects project WHERE project.id = ${input.projectId ?? null}::uuid)
|
|
1354
1431
|
RETURNING *
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1432
|
+
`,
|
|
1433
|
+
);
|
|
1434
|
+
if (!rows[0]) throw new Error("Project does not exist.");
|
|
1435
|
+
return rowToConversation(rows[0]);
|
|
1436
|
+
};
|
|
1437
|
+
if (!input.requestId) return insert(sql);
|
|
1438
|
+
const hash = capabilityRequestHash(normalized);
|
|
1439
|
+
return sql.begin(async (tx) => {
|
|
1440
|
+
const reserved = await tx<{ request_id: string }[]>`
|
|
1441
|
+
INSERT INTO ai.conversation_creations (owner_user_id, request_id, input_hash)
|
|
1442
|
+
VALUES (${input.ownerUserId}::uuid, ${input.requestId}::uuid, ${hash})
|
|
1443
|
+
ON CONFLICT (owner_user_id, request_id) DO NOTHING RETURNING request_id
|
|
1444
|
+
`;
|
|
1445
|
+
const [claim] = await tx<{ input_hash: string; conversation_id: string | null }[]>`
|
|
1446
|
+
SELECT input_hash, conversation_id FROM ai.conversation_creations
|
|
1447
|
+
WHERE owner_user_id = ${input.ownerUserId}::uuid AND request_id = ${input.requestId}::uuid
|
|
1448
|
+
FOR UPDATE
|
|
1449
|
+
`;
|
|
1450
|
+
if (!claim || claim.input_hash !== hash) throw new AiConversationCreationError("conflict");
|
|
1451
|
+
if (!reserved.length) {
|
|
1452
|
+
const [existing] = await tx<ConversationRow[]>`
|
|
1453
|
+
SELECT * FROM ai.conversations WHERE id = ${claim.conversation_id}::uuid
|
|
1454
|
+
AND created_by_user_id = ${input.ownerUserId}::uuid AND archived_at IS NULL
|
|
1455
|
+
`;
|
|
1456
|
+
if (!existing) throw new AiConversationCreationError("unavailable");
|
|
1457
|
+
return rowToConversation(existing);
|
|
1458
|
+
}
|
|
1459
|
+
const conversation = await insert(tx);
|
|
1460
|
+
await tx`UPDATE ai.conversation_creations SET conversation_id = ${conversation.id}::uuid
|
|
1461
|
+
WHERE owner_user_id = ${input.ownerUserId}::uuid AND request_id = ${input.requestId}::uuid`;
|
|
1462
|
+
return conversation;
|
|
1463
|
+
});
|
|
1359
1464
|
},
|
|
1360
1465
|
|
|
1361
1466
|
forkConversation: async (input) => {
|
|
@@ -2442,7 +2547,10 @@ export const aiConversations: AiConversationService = {
|
|
|
2442
2547
|
// summaries are hidden. The active summary sorts after archived rows
|
|
2443
2548
|
// sharing its checkpoint seq, marking where the model context begins.
|
|
2444
2549
|
const rows = await sql<MessageRow[]>`
|
|
2445
|
-
SELECT
|
|
2550
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
2551
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
2552
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
2553
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta
|
|
2446
2554
|
FROM ai.messages
|
|
2447
2555
|
WHERE conversation_id = ${input.conversationId}
|
|
2448
2556
|
AND NOT (kind = 'summary' AND compacted_at IS NOT NULL)
|
|
@@ -2467,7 +2575,10 @@ export const aiConversations: AiConversationService = {
|
|
|
2467
2575
|
ORDER BY seq DESC
|
|
2468
2576
|
LIMIT ${limit}
|
|
2469
2577
|
)
|
|
2470
|
-
SELECT
|
|
2578
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
2579
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
2580
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
2581
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta
|
|
2471
2582
|
FROM ai.messages
|
|
2472
2583
|
WHERE conversation_id = ${input.conversationId}
|
|
2473
2584
|
AND seq IN (SELECT seq FROM page_seqs)
|
|
@@ -2496,7 +2607,10 @@ export const aiConversations: AiConversationService = {
|
|
|
2496
2607
|
const pattern = `%${query.toLowerCase()}%`;
|
|
2497
2608
|
const rows = await sql<(MessageRow & { page_rank: number | string })[]>`
|
|
2498
2609
|
WITH matching AS (
|
|
2499
|
-
SELECT
|
|
2610
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
2611
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
2612
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
2613
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta, DENSE_RANK() OVER (ORDER BY seq DESC) AS page_rank
|
|
2500
2614
|
FROM ai.messages
|
|
2501
2615
|
WHERE conversation_id = ${input.conversationId}::uuid
|
|
2502
2616
|
AND role <> 'tool_result'
|
|
@@ -2620,7 +2734,10 @@ export const aiConversations: AiConversationService = {
|
|
|
2620
2734
|
|
|
2621
2735
|
listContextMessages: async (input) => {
|
|
2622
2736
|
const rows = await sql<MessageRow[]>`
|
|
2623
|
-
SELECT
|
|
2737
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
2738
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
2739
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
2740
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta
|
|
2624
2741
|
FROM ai.messages
|
|
2625
2742
|
WHERE conversation_id = ${input.conversationId}
|
|
2626
2743
|
AND compacted_at IS NULL
|
|
@@ -2631,7 +2748,10 @@ export const aiConversations: AiConversationService = {
|
|
|
2631
2748
|
|
|
2632
2749
|
listTurnMessages: async (input) => {
|
|
2633
2750
|
const rows = await sql<MessageRow[]>`
|
|
2634
|
-
SELECT
|
|
2751
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
2752
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
2753
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
2754
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta
|
|
2635
2755
|
FROM (SELECT * FROM ai.messages UNION ALL SELECT * FROM ai.task_messages) messages
|
|
2636
2756
|
WHERE conversation_id = ${input.conversationId}
|
|
2637
2757
|
AND loop_id = ${input.loopId}
|
|
@@ -3042,7 +3162,7 @@ export const aiConversations: AiConversationService = {
|
|
|
3042
3162
|
const [waits, metadata] = await Promise.all([
|
|
3043
3163
|
activeTurnWaits(rows[0].id),
|
|
3044
3164
|
sql<{ tool_reviews: Record<string, AiStoredActionReview> | null; tool_checks: Record<string, AiActionCheck> | null }[]>`
|
|
3045
|
-
SELECT meta->'toolReviews' AS tool_reviews, meta->'toolChecks' AS tool_checks
|
|
3165
|
+
SELECT ${compactReviewMapSql(sql`meta->'toolReviews'`)} AS tool_reviews, meta->'toolChecks' AS tool_checks
|
|
3046
3166
|
FROM (
|
|
3047
3167
|
SELECT meta, seq, conversation_id, loop_id, role FROM ai.messages
|
|
3048
3168
|
UNION ALL
|
|
@@ -3062,7 +3182,8 @@ export const aiConversations: AiConversationService = {
|
|
|
3062
3182
|
turn: rowToTurn(rows[0]),
|
|
3063
3183
|
liveBlocks: rowToLiveBlocks(rows[0]).map((block) => {
|
|
3064
3184
|
if (block.kind !== "tool") return block;
|
|
3065
|
-
const
|
|
3185
|
+
const snapshot = reviews.get(block.callId);
|
|
3186
|
+
const review = snapshot ? compactStoredReview(snapshot, { ...input, turnId: rows[0]!.id, callId: block.callId }) : undefined;
|
|
3066
3187
|
const check = checks.get(block.callId);
|
|
3067
3188
|
return { ...block, ...(review ? { review } : {}), ...(check ? { check } : {}) };
|
|
3068
3189
|
}),
|
|
@@ -3546,7 +3667,7 @@ export const aiConversations: AiConversationService = {
|
|
|
3546
3667
|
${input.name},
|
|
3547
3668
|
${JSON.stringify(input.args ?? null)}::text::jsonb,
|
|
3548
3669
|
${input.message ?? null},
|
|
3549
|
-
${input.review ?
|
|
3670
|
+
${input.review ? serializeReviewSnapshot("sentence" in input.review ? storedReviewWithContent(input.review, input) : input.review) : null}::text::jsonb,
|
|
3550
3671
|
${input.approvalScope},
|
|
3551
3672
|
${input.allowAlways},
|
|
3552
3673
|
${input.allowChat ?? input.allowAlways},
|
|
@@ -3574,7 +3695,8 @@ export const aiConversations: AiConversationService = {
|
|
|
3574
3695
|
|
|
3575
3696
|
listPendingTurnActions: async (input) => {
|
|
3576
3697
|
const rows = await sql<PendingActionRow[]>`
|
|
3577
|
-
SELECT
|
|
3698
|
+
SELECT approval_id, turn_id, conversation_id, call_id, kind, status, tool_name, args, message,
|
|
3699
|
+
review - 'content' AS review, approval_scope, allow_always, allow_chat, remember_tool_name, frontend_mode, resolved_event
|
|
3578
3700
|
FROM ai.pending_actions
|
|
3579
3701
|
WHERE conversation_id = ${input.conversationId}
|
|
3580
3702
|
AND turn_id = ${input.turnId}
|
|
@@ -3586,7 +3708,8 @@ export const aiConversations: AiConversationService = {
|
|
|
3586
3708
|
|
|
3587
3709
|
getPendingTurnAction: async (input) => {
|
|
3588
3710
|
const rows = await sql<PendingActionRow[]>`
|
|
3589
|
-
SELECT
|
|
3711
|
+
SELECT approval_id, turn_id, conversation_id, call_id, kind, status, tool_name, args, message,
|
|
3712
|
+
review - 'content' AS review, approval_scope, allow_always, allow_chat, remember_tool_name, frontend_mode, resolved_event
|
|
3590
3713
|
FROM ai.pending_actions
|
|
3591
3714
|
WHERE conversation_id = ${input.conversationId}
|
|
3592
3715
|
AND turn_id = ${input.turnId}
|
|
@@ -3610,7 +3733,7 @@ export const aiConversations: AiConversationService = {
|
|
|
3610
3733
|
listTurnToolReviews: async (input) => {
|
|
3611
3734
|
// Only the review metadata, never the messages themselves, which can carry large tool results.
|
|
3612
3735
|
const rows = await sql<{ tool_reviews: Record<string, AiStoredActionReview> | null }[]>`
|
|
3613
|
-
SELECT meta->'toolReviews' AS tool_reviews
|
|
3736
|
+
SELECT ${compactReviewMapSql(sql`meta->'toolReviews'`)} AS tool_reviews
|
|
3614
3737
|
FROM (
|
|
3615
3738
|
SELECT meta, seq, conversation_id, loop_id, role FROM ai.messages
|
|
3616
3739
|
UNION ALL
|
|
@@ -3622,12 +3745,19 @@ export const aiConversations: AiConversationService = {
|
|
|
3622
3745
|
AND meta ? 'toolReviews'
|
|
3623
3746
|
ORDER BY seq ASC
|
|
3624
3747
|
`;
|
|
3625
|
-
return new Map(
|
|
3748
|
+
return new Map(
|
|
3749
|
+
rows.flatMap((row) =>
|
|
3750
|
+
Object.entries(row.tool_reviews ?? {}).map(
|
|
3751
|
+
([callId, review]) => [callId, compactStoredReview(review, { ...input, callId })] as const,
|
|
3752
|
+
),
|
|
3753
|
+
),
|
|
3754
|
+
);
|
|
3626
3755
|
},
|
|
3627
3756
|
|
|
3628
3757
|
listPendingActionRecords: async (input) => {
|
|
3629
3758
|
const rows = await sql<PendingActionRow[]>`
|
|
3630
|
-
SELECT
|
|
3759
|
+
SELECT approval_id, turn_id, conversation_id, call_id, kind, status, tool_name, args, message,
|
|
3760
|
+
review - 'content' AS review, approval_scope, allow_always, allow_chat, remember_tool_name, frontend_mode, resolved_event
|
|
3631
3761
|
FROM ai.pending_actions
|
|
3632
3762
|
WHERE conversation_id = ${input.conversationId}
|
|
3633
3763
|
AND turn_id = ${input.turnId}
|
|
@@ -3639,7 +3769,8 @@ export const aiConversations: AiConversationService = {
|
|
|
3639
3769
|
|
|
3640
3770
|
listResolvedPendingActions: async (input) => {
|
|
3641
3771
|
const rows = await sql<PendingActionRow[]>`
|
|
3642
|
-
SELECT
|
|
3772
|
+
SELECT approval_id, turn_id, conversation_id, call_id, kind, status, tool_name, args, message,
|
|
3773
|
+
review - 'content' AS review, approval_scope, allow_always, allow_chat, remember_tool_name, frontend_mode, resolved_event
|
|
3643
3774
|
FROM ai.pending_actions
|
|
3644
3775
|
WHERE conversation_id = ${input.conversationId}
|
|
3645
3776
|
AND turn_id = ${input.turnId}
|
|
@@ -3803,7 +3934,11 @@ export const aiConversations: AiConversationService = {
|
|
|
3803
3934
|
const rows = input.turnId
|
|
3804
3935
|
? (
|
|
3805
3936
|
await sql<MessageRow[]>`
|
|
3806
|
-
SELECT
|
|
3937
|
+
SELECT id, short_id, conversation_id, seq, kind, message, loop_id, model_profile_id, provider_model,
|
|
3938
|
+
usage, stop_reason, loop_aggregate, loop_done_reason, compacted_at, created_at,
|
|
3939
|
+
feedback_rating, feedback_reasons, feedback_comment, feedback_updated_at,
|
|
3940
|
+
${compactReviewMetaSql(todoMetaJson())} AS meta
|
|
3941
|
+
FROM ai.messages
|
|
3807
3942
|
WHERE conversation_id = ${input.conversationId}::uuid AND compacted_at IS NULL
|
|
3808
3943
|
AND ${visibleToTurn(input.turnId)}
|
|
3809
3944
|
ORDER BY seq ASC
|
package/src/ai/types.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { z } from "zod";
|
|
|
15
15
|
import type { CapabilitySentenceField } from "../_internal/capability-sentences";
|
|
16
16
|
import type {
|
|
17
17
|
CapabilityActionCallReview,
|
|
18
|
-
|
|
18
|
+
CapabilityActionReviewPresentation,
|
|
19
19
|
CapabilityActionSentences,
|
|
20
20
|
CloudResourceRef,
|
|
21
21
|
} from "../contracts/capabilities";
|
|
@@ -457,7 +457,7 @@ export type AiPendingTurnAction =
|
|
|
457
457
|
args: unknown;
|
|
458
458
|
message?: string;
|
|
459
459
|
/** Stored review, including the declared ask reason for new-shape reviews. */
|
|
460
|
-
review?:
|
|
460
|
+
review?: CapabilityActionReviewPresentation;
|
|
461
461
|
/** Owning application, with its registry name in the turn's request locale. */
|
|
462
462
|
app?: { id: string; name: string };
|
|
463
463
|
/** The chat's current title, when known. */
|
|
@@ -499,7 +499,7 @@ export type AiPendingTurnActionRecord = {
|
|
|
499
499
|
name: string;
|
|
500
500
|
args: unknown;
|
|
501
501
|
message?: string;
|
|
502
|
-
review?:
|
|
502
|
+
review?: CapabilityActionReviewPresentation;
|
|
503
503
|
approvalScope: string;
|
|
504
504
|
allowAlways: boolean;
|
|
505
505
|
/** Whether the approval can be remembered for this chat; defaults to `allowAlways`. */
|
|
@@ -736,7 +736,14 @@ export type AiTurnSweepResult = {
|
|
|
736
736
|
};
|
|
737
737
|
|
|
738
738
|
export type AiConversationService = {
|
|
739
|
+
getDraftSubmission(input: {
|
|
740
|
+
conversationId: string;
|
|
741
|
+
ownerUserId: string;
|
|
742
|
+
revision: number;
|
|
743
|
+
}): Promise<{ messageId: string; turnId: string | null } | null>;
|
|
739
744
|
createConversation(input: {
|
|
745
|
+
/** Stable identity for one logical creation; replay never creates a replacement. */
|
|
746
|
+
requestId?: string;
|
|
740
747
|
ownerUserId: string;
|
|
741
748
|
title?: string;
|
|
742
749
|
description?: string;
|
package/src/ai/usage.ts
CHANGED
|
@@ -317,8 +317,8 @@ export const aiUsage = {
|
|
|
317
317
|
: field === "appId"
|
|
318
318
|
? sql`e.app_id`
|
|
319
319
|
: sql`e.task`;
|
|
320
|
-
const label = field === "userId" ? sql`COALESCE(NULLIF(u.display_name,''),u.uid,sa.name
|
|
321
|
-
return sql<{ id: string; label: string }[]>`SELECT DISTINCT ${key} AS id,${label} AS label FROM (${events(since, until)}) e
|
|
320
|
+
const label = field === "userId" ? sql`COALESCE(NULLIF(u.display_name,''),u.uid,sa.name)` : key;
|
|
321
|
+
return sql<{ id: string; label: string | null }[]>`SELECT DISTINCT ${key} AS id,${label} AS label FROM (${events(since, until)}) e
|
|
322
322
|
LEFT JOIN auth.users u ON u.id=e.user_id LEFT JOIN auth.service_accounts sa ON sa.id=e.user_id WHERE ${key} IS NOT NULL
|
|
323
323
|
AND (position(lower(${search}) in lower(${label}))>0 OR ${key}=${search} OR (${field === "userId"} AND position(lower(${search}) in lower(COALESCE(u.uid,'')))>0))
|
|
324
324
|
ORDER BY label,id LIMIT ${query.perPage}`;
|
|
@@ -39,6 +39,7 @@ import { GotenbergRenderError, testGotenberg } from "../services/pdf";
|
|
|
39
39
|
import * as settings from "../services/settings";
|
|
40
40
|
import { SETTINGS_MAP, validateSettingValue } from "../services/settings/defaults";
|
|
41
41
|
|
|
42
|
+
import { assertGeoBudgetPair, GeoBudgetSettingError } from "../services/settings/geo-budget";
|
|
42
43
|
import { invalidateSettingsCacheForAdmin } from "../services/settings/store";
|
|
43
44
|
|
|
44
45
|
const BulkSaveSchema = z.union([
|
|
@@ -67,6 +68,9 @@ const settingsNotSavedMessage = (locale: string): string =>
|
|
|
67
68
|
locale.startsWith("de") ? "Die Einstellungen wurden nicht gespeichert." : "The settings were not saved.";
|
|
68
69
|
|
|
69
70
|
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
71
|
+
// The Geoapify key is written only where Cloud verifies it with the provider first, so a refused key never replaces a working one.
|
|
72
|
+
const GEO_KEY = "geo.api_key";
|
|
73
|
+
const GEO_KEY_DEDICATED = "Set or remove the Geoapify key with /api/admin/core/geo/key, which verifies it first.";
|
|
70
74
|
|
|
71
75
|
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
72
76
|
const AI_ENABLED_KEY = "ai.enabled";
|
|
@@ -333,7 +337,10 @@ const app = new Hono<AuthContext>()
|
|
|
333
337
|
return c.json({ ok: true, bytes: result.bytes, contentType: result.contentType });
|
|
334
338
|
} catch (error) {
|
|
335
339
|
if (error instanceof GotenbergRenderError) {
|
|
336
|
-
return c.json(
|
|
340
|
+
return c.json(
|
|
341
|
+
{ message: error.message, code: error.code, status: error.status },
|
|
342
|
+
error.code === "busy" ? 503 : error.code === "not_configured" ? 400 : 502,
|
|
343
|
+
);
|
|
337
344
|
}
|
|
338
345
|
return c.json({ message: "Failed to test Gotenberg PDF rendering" }, 500);
|
|
339
346
|
}
|
|
@@ -384,6 +391,7 @@ const app = new Hono<AuthContext>()
|
|
|
384
391
|
ownership[key] = `Setting "${key}" cannot be updated and reset in the same save`;
|
|
385
392
|
}
|
|
386
393
|
}
|
|
394
|
+
if (keys.includes(GEO_KEY)) ownership[GEO_KEY] = GEO_KEY_DEDICATED;
|
|
387
395
|
if (Object.keys(ownership).length > 0) {
|
|
388
396
|
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
389
397
|
}
|
|
@@ -469,11 +477,12 @@ const app = new Hono<AuthContext>()
|
|
|
469
477
|
// Every settings and AI credential write runs on the transaction's own
|
|
470
478
|
// connection, so a failure rolls the whole save back.
|
|
471
479
|
await sql.begin(async (tx) => {
|
|
480
|
+
await assertGeoBudgetPair(tx, validatedValues, resets);
|
|
472
481
|
for (const [key, value] of Object.entries(validatedValues)) {
|
|
473
482
|
try {
|
|
474
483
|
await settings.set(key, value, tx, { replaceUnreadable: replace.has(key) });
|
|
475
484
|
} catch (error) {
|
|
476
|
-
fieldErrors[key] = `Failed to
|
|
485
|
+
fieldErrors[key] = `Failed to save ${key}`;
|
|
477
486
|
throw error;
|
|
478
487
|
}
|
|
479
488
|
}
|
|
@@ -514,6 +523,7 @@ const app = new Hono<AuthContext>()
|
|
|
514
523
|
}
|
|
515
524
|
});
|
|
516
525
|
} catch (error) {
|
|
526
|
+
if (error instanceof GeoBudgetSettingError) return error.getResponse();
|
|
517
527
|
if (error instanceof settings.SettingUnreadableError) {
|
|
518
528
|
return c.json(
|
|
519
529
|
{
|
|
@@ -553,6 +563,7 @@ const app = new Hono<AuthContext>()
|
|
|
553
563
|
if (!isKnownSetting(key)) {
|
|
554
564
|
return c.json({ message: `Unknown setting "${key}"` }, 400);
|
|
555
565
|
}
|
|
566
|
+
if (key === GEO_KEY) return c.json({ message: GEO_KEY_DEDICATED, errors: { [GEO_KEY]: GEO_KEY_DEDICATED } }, 400);
|
|
556
567
|
|
|
557
568
|
let aiPlan: AiSettingsMutationPlan;
|
|
558
569
|
try {
|
|
@@ -567,6 +578,7 @@ const app = new Hono<AuthContext>()
|
|
|
567
578
|
|
|
568
579
|
try {
|
|
569
580
|
await sql.begin(async (tx) => {
|
|
581
|
+
await assertGeoBudgetPair(tx, {}, [key]);
|
|
570
582
|
await settings.remove(key, tx);
|
|
571
583
|
if (key.startsWith("user.category."))
|
|
572
584
|
await audit.record(
|
|
@@ -588,6 +600,7 @@ const app = new Hono<AuthContext>()
|
|
|
588
600
|
}
|
|
589
601
|
});
|
|
590
602
|
} catch (error) {
|
|
603
|
+
if (error instanceof GeoBudgetSettingError || error instanceof settings.SettingUnreadableError) return error.getResponse();
|
|
591
604
|
console.error(`[settings] failed to reset "${key}"`, error);
|
|
592
605
|
return c.json({ message: "Reset failed" }, 500);
|
|
593
606
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { GeoBudgetInputSchema, GeoKeyInputSchema } from "../contracts/geo-admin";
|
|
4
|
+
import { type AuthContext, auth, v } from "../server";
|
|
5
|
+
import { GeoAdminError, geoAdmin } from "../services/geo/admin";
|
|
6
|
+
|
|
7
|
+
export const createAdminGeoRoutes = (
|
|
8
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
9
|
+
service: Pick<typeof geoAdmin, "status" | "usage" | "setKey" | "removeKey" | "saveBudget"> = geoAdmin,
|
|
10
|
+
) =>
|
|
11
|
+
new Hono<AuthContext>()
|
|
12
|
+
.use(authenticate)
|
|
13
|
+
.onError((error, c) => {
|
|
14
|
+
if (error instanceof GeoAdminError) return c.json(error.body, error.status);
|
|
15
|
+
if (c.req.raw.signal.aborted) return new Response(null, { status: 499 });
|
|
16
|
+
throw error;
|
|
17
|
+
})
|
|
18
|
+
.get(
|
|
19
|
+
"/",
|
|
20
|
+
describeRoute({ tags: ["Places and maps"], summary: "Show the Geoapify key state, plan and budgets" }),
|
|
21
|
+
auth.requireOAuthScope("read", "admin"),
|
|
22
|
+
async (c) => c.json(await service.status(c.get("user"))),
|
|
23
|
+
)
|
|
24
|
+
.get(
|
|
25
|
+
"/usage",
|
|
26
|
+
describeRoute({ tags: ["Places and maps"], summary: "Show this month's and today's geo credits with shares per function" }),
|
|
27
|
+
auth.requireOAuthScope("read", "admin"),
|
|
28
|
+
async (c) => c.json(await service.usage(c.get("user"))),
|
|
29
|
+
)
|
|
30
|
+
.put(
|
|
31
|
+
"/key",
|
|
32
|
+
describeRoute({
|
|
33
|
+
tags: ["Places and maps"],
|
|
34
|
+
summary: "Verify a Geoapify key with one provider lookup, then store it; a refused key changes nothing",
|
|
35
|
+
}),
|
|
36
|
+
auth.requireOAuthScope("write", "admin"),
|
|
37
|
+
v("json", GeoKeyInputSchema),
|
|
38
|
+
async (c) => c.json(await service.setKey(c.get("user"), c.req.valid("json").apiKey, c.req.raw.signal)),
|
|
39
|
+
)
|
|
40
|
+
.delete(
|
|
41
|
+
"/key",
|
|
42
|
+
describeRoute({ tags: ["Places and maps"], summary: "Remove the Geoapify key; places and maps turn off for everyone" }),
|
|
43
|
+
auth.requireOAuthScope("write", "admin"),
|
|
44
|
+
async (c) => c.json(await service.removeKey(c.get("user"))),
|
|
45
|
+
)
|
|
46
|
+
.put(
|
|
47
|
+
"/budget",
|
|
48
|
+
describeRoute({ tags: ["Places and maps"], summary: "Save the plan, the monthly budget and the daily limit per person" }),
|
|
49
|
+
auth.requireOAuthScope("write", "admin"),
|
|
50
|
+
v("json", GeoBudgetInputSchema),
|
|
51
|
+
async (c) => c.json(await service.saveBudget(c.get("user"), c.req.valid("json"))),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export default createAdminGeoRoutes();
|
|
@@ -6,7 +6,9 @@ import { type AuthContext, auth, v } from "../server";
|
|
|
6
6
|
import { PosixError, posix } from "../services/accounts/posix";
|
|
7
7
|
|
|
8
8
|
const Id = z.object({ id: z.uuid() });
|
|
9
|
-
const Configure = z
|
|
9
|
+
const Configure = z
|
|
10
|
+
.object({ config: LinuxIdentityConfigurationSchema, rangeReserved: z.boolean(), replaceUnreadable: z.boolean().optional() })
|
|
11
|
+
.strict();
|
|
10
12
|
|
|
11
13
|
export const createAdminLinuxIdentityRoutes = (
|
|
12
14
|
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
@@ -32,12 +34,13 @@ export const createAdminLinuxIdentityRoutes = (
|
|
|
32
34
|
describeRoute({
|
|
33
35
|
tags: ["Linux identities"],
|
|
34
36
|
summary: "Configure automatic local Linux identity assignment; does not enable computer login",
|
|
37
|
+
description: "Unreadable stored configuration returns 409 setting_unreadable unless replaceUnreadable is explicitly true.",
|
|
35
38
|
}),
|
|
36
39
|
v("json", Configure),
|
|
37
40
|
async (c) => {
|
|
38
|
-
const { config, rangeReserved } = c.req.valid("json");
|
|
41
|
+
const { config, rangeReserved, replaceUnreadable } = c.req.valid("json");
|
|
39
42
|
if (config.enabled && !rangeReserved) throw new PosixError("range_confirmation_required", 400);
|
|
40
|
-
return c.json(await service.configure(c.get("user"), config));
|
|
43
|
+
return c.json(await service.configure(c.get("user"), config, { replaceUnreadable }));
|
|
41
44
|
},
|
|
42
45
|
)
|
|
43
46
|
.get(
|