@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
package/src/ai/types.ts
CHANGED
|
@@ -13,11 +13,32 @@ import type {
|
|
|
13
13
|
import type { Usage } from "@k2b/nessi/ai";
|
|
14
14
|
import type { z } from "zod";
|
|
15
15
|
import type { CapabilitySentenceField } from "../_internal/capability-sentences";
|
|
16
|
-
import type {
|
|
16
|
+
import type { CapabilityActionReviewData, CapabilityActionSentences, CloudResourceRef } from "../contracts/capabilities";
|
|
17
17
|
import type { RequestActor } from "../server";
|
|
18
|
+
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
18
19
|
import type { AiLocalClient, AiLocalContext, AiLocalToolName } from "./local-tool-contracts";
|
|
19
20
|
import type { AiTurnBlock } from "./protocol";
|
|
20
21
|
|
|
22
|
+
/** Current reader-confirmed metadata for a recently sent resource. */
|
|
23
|
+
export type AiRecentResource = {
|
|
24
|
+
ref: CloudResourceRef;
|
|
25
|
+
title: string;
|
|
26
|
+
icon?: string;
|
|
27
|
+
href?: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
preview?: string;
|
|
30
|
+
appName: string;
|
|
31
|
+
count: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AiRecentSentResourceRef = {
|
|
35
|
+
ref: CloudResourceRef;
|
|
36
|
+
count: number;
|
|
37
|
+
lastSentAt: string;
|
|
38
|
+
/** Internal conversation UUID, only for authorized Studio reads. */
|
|
39
|
+
conversationId: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
21
42
|
export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision", "transcription"] as const;
|
|
22
43
|
export type AiModelCapability = (typeof AI_MODEL_CAPABILITIES)[number];
|
|
23
44
|
|
|
@@ -298,12 +319,18 @@ export type AiStoredMessage = {
|
|
|
298
319
|
sourceHref?: string;
|
|
299
320
|
};
|
|
300
321
|
scheduledTask?: {
|
|
322
|
+
reason?: import("./chat-task-reasons").ChatTaskReason;
|
|
323
|
+
/** The message text is only the reason sentence; the chat words the reason itself instead. */
|
|
324
|
+
reasonOnly?: true;
|
|
325
|
+
stop?: { cause: "paused" | "deleted"; done: string[] };
|
|
301
326
|
taskId: string;
|
|
302
327
|
occurrenceId: string;
|
|
303
328
|
scheduledFor: string;
|
|
304
329
|
trigger: "scheduled" | "manual";
|
|
305
330
|
};
|
|
306
331
|
toolPresentations?: Record<string, AiToolPresentation>;
|
|
332
|
+
toolReviews?: Record<string, AiStoredActionReview>;
|
|
333
|
+
toolChecks?: Record<string, AiActionCheck>;
|
|
307
334
|
/** How the person decided an approval; `expired` when the turn ended while it still waited. */
|
|
308
335
|
toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
|
|
309
336
|
/** Requests a website approval let through, by tool call and then by `AiWebsiteReceipt.index`. */
|
|
@@ -424,7 +451,7 @@ export type AiPendingTurnAction =
|
|
|
424
451
|
name: string;
|
|
425
452
|
args: unknown;
|
|
426
453
|
message?: string;
|
|
427
|
-
review?:
|
|
454
|
+
review?: CapabilityActionReviewData;
|
|
428
455
|
allowAlways: boolean;
|
|
429
456
|
/** Whether the approval can be remembered for this chat. */
|
|
430
457
|
allowChat: boolean;
|
|
@@ -460,7 +487,7 @@ export type AiPendingTurnActionRecord = {
|
|
|
460
487
|
name: string;
|
|
461
488
|
args: unknown;
|
|
462
489
|
message?: string;
|
|
463
|
-
review?:
|
|
490
|
+
review?: CapabilityActionReviewData;
|
|
464
491
|
approvalScope: string;
|
|
465
492
|
allowAlways: boolean;
|
|
466
493
|
/** Whether the approval can be remembered for this chat; defaults to `allowAlways`. */
|
|
@@ -613,6 +640,11 @@ export type AiConversationFileSnapshot = {
|
|
|
613
640
|
};
|
|
614
641
|
|
|
615
642
|
export type AiBackgroundRunContext = {
|
|
643
|
+
scheduledFor?: string;
|
|
644
|
+
trigger?: "scheduled" | "manual";
|
|
645
|
+
timezone?: string;
|
|
646
|
+
stopDeliveredAt?: string;
|
|
647
|
+
needsApproval?: { action: string };
|
|
616
648
|
taskId: string;
|
|
617
649
|
occurrenceId: string;
|
|
618
650
|
context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
|
|
@@ -757,6 +789,7 @@ export type AiConversationService = {
|
|
|
757
789
|
before?: string;
|
|
758
790
|
limit?: number;
|
|
759
791
|
}): Promise<{ resources: AiConversationResourceRef[]; nextCursor?: string }>;
|
|
792
|
+
listRecentSentResourceRefs(input: { ownerUserId: string }): Promise<AiRecentSentResourceRef[]>;
|
|
760
793
|
listUserConversationResources(input: {
|
|
761
794
|
ownerUserId: string;
|
|
762
795
|
search?: string;
|
|
@@ -977,7 +1010,10 @@ export type AiConversationService = {
|
|
|
977
1010
|
seq: number;
|
|
978
1011
|
}): Promise<boolean>;
|
|
979
1012
|
/** Record an abort wish. The caller finalizes ownerless turns itself. */
|
|
980
|
-
requestTurnAbort(
|
|
1013
|
+
requestTurnAbort(
|
|
1014
|
+
input: { conversationId: string; turnId: string; reason?: string },
|
|
1015
|
+
options?: { db: import("bun").SQL },
|
|
1016
|
+
): Promise<AiTurnAbortRequest>;
|
|
981
1017
|
completeTurn(input: {
|
|
982
1018
|
conversationId: string;
|
|
983
1019
|
turnId: string;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
UserProviderSchema,
|
|
15
15
|
} from "../contracts";
|
|
16
16
|
import { type AuthContext, auth, expectUserBackedActor, jsonResponse, requiresAuth, respond, v } from "../server";
|
|
17
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
17
18
|
import { accountsAppService as accountsService } from "../services";
|
|
18
19
|
|
|
19
20
|
const EntitiesListResponseSchema = z.object({
|
|
@@ -91,7 +92,7 @@ type AccountsEntitiesRouteDependencies = {
|
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRouteDependencies = {}) =>
|
|
94
|
-
new Hono<AuthContext>().get(
|
|
95
|
+
new Hono<AuthContext>().use("/entities/*", noTransform()).get(
|
|
95
96
|
"/entities",
|
|
96
97
|
dependencies.authenticate ?? auth.requireRole("authenticated"),
|
|
97
98
|
dependencies.requireUser ?? auth.requireUser(),
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { AiProjectLastAdminError, aiProjects } from "../ai/projects";
|
|
4
4
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
5
5
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
7
|
|
|
7
8
|
const ListProjectsSchema = z
|
|
8
9
|
.object({
|
|
@@ -20,6 +21,7 @@ const lastAdminConflict = (c: Parameters<typeof respond>[0], error: AiProjectLas
|
|
|
20
21
|
|
|
21
22
|
export const createAdminAiProjectsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
22
23
|
new Hono<AuthContext>()
|
|
24
|
+
.use("*", noTransform())
|
|
23
25
|
.use("*", authenticate)
|
|
24
26
|
.get("/", v("query", ListProjectsSchema), async (c) => {
|
|
25
27
|
const query = c.req.valid("query");
|
|
@@ -4,6 +4,7 @@ import { aiAppSkills } from "../ai/app-skill-store";
|
|
|
4
4
|
import { AiSkillInputError, AiSkillLastAdminError, AiSkillRevisionConflictError, aiSkills } from "../ai/skills";
|
|
5
5
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
6
6
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
7
8
|
|
|
8
9
|
const ListSkillsSchema = z
|
|
9
10
|
.object({
|
|
@@ -32,6 +33,7 @@ const installAppSkill = async (c: Parameters<typeof respond>[0], install: () =>
|
|
|
32
33
|
|
|
33
34
|
export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
34
35
|
new Hono<AuthContext>()
|
|
36
|
+
.use("*", noTransform())
|
|
35
37
|
.use("*", authenticate)
|
|
36
38
|
.get("/", v("query", ListSkillsSchema), async (c) => {
|
|
37
39
|
const query = c.req.valid("query");
|
|
@@ -3,6 +3,7 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { aiUsage } from "../ai/usage";
|
|
5
5
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
7
|
import { AiUsageQuerySchema } from "../shared/ai-usage";
|
|
7
8
|
|
|
8
9
|
export const createAdminAiUsageRoutes = (
|
|
@@ -10,6 +11,7 @@ export const createAdminAiUsageRoutes = (
|
|
|
10
11
|
service: typeof aiUsage = aiUsage,
|
|
11
12
|
) =>
|
|
12
13
|
new Hono<AuthContext>()
|
|
14
|
+
.use("*", noTransform())
|
|
13
15
|
.use("*", authenticate)
|
|
14
16
|
.get(
|
|
15
17
|
"/report",
|
|
@@ -46,9 +46,10 @@ const BulkSaveSchema = z.union([
|
|
|
46
46
|
.object({
|
|
47
47
|
updates: z.record(z.string(), z.unknown()).optional().default({}),
|
|
48
48
|
resets: z.array(z.string()).optional().default([]),
|
|
49
|
+
replaceUnreadable: z.array(z.string()).optional().default([]),
|
|
49
50
|
})
|
|
50
51
|
.strict(),
|
|
51
|
-
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[] })),
|
|
52
|
+
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[], replaceUnreadable: [] as string[] })),
|
|
52
53
|
]);
|
|
53
54
|
const LEGAL_DOCUMENTS = [
|
|
54
55
|
{ kind: "terms", path: "/legal/terms" },
|
|
@@ -58,6 +59,13 @@ const LEGAL_DOCUMENTS = [
|
|
|
58
59
|
|
|
59
60
|
type FieldErrors = Record<string, string>;
|
|
60
61
|
|
|
62
|
+
const unreadableSettingMessage = (locale: string): string =>
|
|
63
|
+
locale.startsWith("de")
|
|
64
|
+
? "Der gespeicherte Wert kann von dieser Anwendung nicht gelesen werden. Ersetze ihn ausdrücklich oder nutze den Standardwert."
|
|
65
|
+
: "The stored value cannot be read by this application. Replace it explicitly or use the default.";
|
|
66
|
+
const settingsNotSavedMessage = (locale: string): string =>
|
|
67
|
+
locale.startsWith("de") ? "Die Einstellungen wurden nicht gespeichert." : "The settings were not saved.";
|
|
68
|
+
|
|
61
69
|
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
62
70
|
|
|
63
71
|
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
@@ -335,168 +343,211 @@ const app = new Hono<AuthContext>()
|
|
|
335
343
|
if (result.ok) return c.json({ ok: true });
|
|
336
344
|
return c.json({ message: result.error, kind: result.kind }, result.status);
|
|
337
345
|
})
|
|
338
|
-
.put(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
346
|
+
.put(
|
|
347
|
+
"/",
|
|
348
|
+
auth.requireRole("admin"),
|
|
349
|
+
describeRoute({
|
|
350
|
+
tags: ["Administration"],
|
|
351
|
+
summary: "Save platform settings",
|
|
352
|
+
...requiresAdmin,
|
|
353
|
+
responses: {
|
|
354
|
+
204: { description: "Settings saved" },
|
|
355
|
+
400: jsonResponse(z.object({ message: z.string(), errors: z.record(z.string(), z.string()).optional() }), "Invalid settings"),
|
|
356
|
+
409: jsonResponse(
|
|
357
|
+
z.object({ message: z.string(), errors: z.record(z.string(), z.string()), code: z.literal("setting_unreadable").optional() }),
|
|
358
|
+
"Stored setting unreadable or model access conflict",
|
|
359
|
+
),
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
v("json", BulkSaveSchema),
|
|
363
|
+
async (c) => {
|
|
364
|
+
const { updates, resets, replaceUnreadable } = c.req.valid("json");
|
|
365
|
+
const replace = new Set(replaceUnreadable);
|
|
366
|
+
const updateKeys = Object.keys(updates);
|
|
367
|
+
const keys = [...updateKeys, ...resets];
|
|
342
368
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
369
|
+
if (keys.length === 0 && replace.size === 0) {
|
|
370
|
+
return c.body(null, 204);
|
|
371
|
+
}
|
|
346
372
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
const ownership: FieldErrors = {};
|
|
374
|
+
for (const key of keys) {
|
|
375
|
+
if (!isKnownSetting(key)) {
|
|
376
|
+
ownership[key] = `Unknown setting "${key}"`;
|
|
377
|
+
}
|
|
351
378
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
379
|
+
for (const key of replace) {
|
|
380
|
+
if (!Object.hasOwn(updates, key)) ownership[key] = `Setting "${key}" must be included in updates to replace its unreadable value`;
|
|
381
|
+
}
|
|
382
|
+
for (const key of resets) {
|
|
383
|
+
if (key in updates) {
|
|
384
|
+
ownership[key] = `Setting "${key}" cannot be updated and reset in the same save`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(ownership).length > 0) {
|
|
388
|
+
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
356
389
|
}
|
|
357
|
-
}
|
|
358
|
-
if (Object.keys(ownership).length > 0) {
|
|
359
|
-
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
360
|
-
}
|
|
361
390
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Unsplittable means the keys could not be separated out, and storing the
|
|
367
|
-
// value as posted would put them back in a setting the browser reads.
|
|
368
|
-
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
369
|
-
}
|
|
370
|
-
let accessChanges: AiModelAccessChange[] = [];
|
|
371
|
-
if (aiSplit) {
|
|
372
|
-
try {
|
|
373
|
-
aiSplit.profilesJson = prepareAiModelProfileImages(aiSplit.profilesJson);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
if (error instanceof ImageMetadataError)
|
|
376
|
-
return c.json({ message: error.message, code: error.code, errors: { [AI_PROFILES_KEY]: error.message } }, 422);
|
|
377
|
-
throw error;
|
|
391
|
+
const states = await settings.readStates(updateKeys);
|
|
392
|
+
const unreadable: FieldErrors = {};
|
|
393
|
+
for (const key of updateKeys) {
|
|
394
|
+
if (states.get(key)?.stored === "unreadable" && !replace.has(key)) unreadable[key] = unreadableSettingMessage(getLocale(c));
|
|
378
395
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
aiSplit.profilesJson = split.profilesJson;
|
|
382
|
-
accessChanges = split.changes;
|
|
383
|
-
} catch {
|
|
384
|
-
return c.json(
|
|
385
|
-
{
|
|
386
|
-
message: "Invalid model permissions",
|
|
387
|
-
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
388
|
-
},
|
|
389
|
-
400,
|
|
390
|
-
);
|
|
396
|
+
if (Object.keys(unreadable).length > 0) {
|
|
397
|
+
return c.json({ message: settingsNotSavedMessage(getLocale(c)), errors: unreadable, code: "setting_unreadable" }, 409);
|
|
391
398
|
}
|
|
392
|
-
}
|
|
393
|
-
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
394
399
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (!def) continue;
|
|
403
|
-
const validated = validateSettingValue(def, value);
|
|
404
|
-
if (validated.ok) validatedValues[key] = validated.value;
|
|
405
|
-
else invalid[key] = validated.error;
|
|
406
|
-
if (key === "freeipa.ca_cert") {
|
|
407
|
-
const certificate = validateFreeIpaCaCert(value);
|
|
408
|
-
if (!certificate.ok) invalid[key] = certificate.error;
|
|
400
|
+
// Strip AI provider keys before anything is validated or written, so the
|
|
401
|
+
// value that gets checked is the value that gets stored.
|
|
402
|
+
const aiSplit = AI_PROFILES_KEY in updates ? splitAiProfileCredentials(updates[AI_PROFILES_KEY]) : null;
|
|
403
|
+
if (AI_PROFILES_KEY in updates && !aiSplit) {
|
|
404
|
+
// Unsplittable means the keys could not be separated out, and storing the
|
|
405
|
+
// value as posted would put them back in a setting the browser reads.
|
|
406
|
+
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
409
407
|
}
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
let accessChanges: AiModelAccessChange[] = [];
|
|
409
|
+
if (aiSplit) {
|
|
410
|
+
try {
|
|
411
|
+
aiSplit.profilesJson = prepareAiModelProfileImages(aiSplit.profilesJson);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof ImageMetadataError)
|
|
414
|
+
return c.json({ message: error.message, code: error.code, errors: { [AI_PROFILES_KEY]: error.message } }, 422);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const split = splitAiModelAccess(aiSplit.profilesJson);
|
|
419
|
+
aiSplit.profilesJson = split.profilesJson;
|
|
420
|
+
accessChanges = split.changes;
|
|
421
|
+
} catch {
|
|
422
|
+
return c.json(
|
|
423
|
+
{
|
|
424
|
+
message: "Invalid model permissions",
|
|
425
|
+
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
426
|
+
},
|
|
427
|
+
400,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
412
430
|
}
|
|
413
|
-
|
|
414
|
-
if (Object.keys(invalid).length > 0) {
|
|
415
|
-
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
416
|
-
}
|
|
431
|
+
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
417
432
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
// Validate everything up front. settings.set validates as it writes, so a
|
|
434
|
+
// single bad field used to leave the earlier keys already saved — with the
|
|
435
|
+
// AI profiles among them, whose save also prunes credentials.
|
|
436
|
+
const invalid: FieldErrors = {};
|
|
437
|
+
const validatedValues: Record<string, unknown> = {};
|
|
438
|
+
for (const [key, value] of Object.entries(finalValues)) {
|
|
439
|
+
const def = SETTINGS_MAP.get(key);
|
|
440
|
+
if (!def) continue;
|
|
441
|
+
const validated = validateSettingValue(def, value);
|
|
442
|
+
if (validated.ok) validatedValues[key] = validated.value;
|
|
443
|
+
else invalid[key] = validated.error;
|
|
444
|
+
if (key === "freeipa.ca_cert") {
|
|
445
|
+
const certificate = validateFreeIpaCaCert(value);
|
|
446
|
+
if (!certificate.ok) invalid[key] = certificate.error;
|
|
447
|
+
}
|
|
448
|
+
if (INTEGER_FREEIPA_SETTINGS.has(key) && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
449
|
+
invalid[key] = "Value must be a whole number";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (Object.keys(invalid).length > 0) {
|
|
453
|
+
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
454
|
+
}
|
|
428
455
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
456
|
+
let aiPlan: AiSettingsMutationPlan;
|
|
457
|
+
try {
|
|
458
|
+
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit, getLocale(c));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
461
|
+
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
462
|
+
}
|
|
463
|
+
if (Object.keys(aiPlan.errors).length > 0) {
|
|
464
|
+
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const fieldErrors: FieldErrors = {};
|
|
468
|
+
try {
|
|
469
|
+
// Every settings and AI credential write runs on the transaction's own
|
|
470
|
+
// connection, so a failure rolls the whole save back.
|
|
471
|
+
await sql.begin(async (tx) => {
|
|
472
|
+
for (const [key, value] of Object.entries(validatedValues)) {
|
|
473
|
+
try {
|
|
474
|
+
await settings.set(key, value, tx, { replaceUnreadable: replace.has(key) });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
fieldErrors[key] = `Failed to update ${key}`;
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
440
479
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
480
|
+
for (const key of resets) {
|
|
481
|
+
try {
|
|
482
|
+
await settings.remove(key, tx);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
fieldErrors[key] = `Failed to reset ${key}`;
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
448
487
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
488
|
+
const categoryKeys = keys.filter((key) => key.startsWith("user.category."));
|
|
489
|
+
if (categoryKeys.length > 0)
|
|
490
|
+
await audit.record(
|
|
491
|
+
{
|
|
492
|
+
action: "accounts.categories.configure",
|
|
493
|
+
outcome: "allowed",
|
|
494
|
+
actor: { userId: c.get("user").id },
|
|
495
|
+
target: { type: "settings", id: "user.category" },
|
|
496
|
+
metadata: {
|
|
497
|
+
keys: categoryKeys,
|
|
498
|
+
updates: Object.fromEntries(
|
|
499
|
+
categoryKeys.filter((key) => key in validatedValues).map((key) => [key, validatedValues[key]]),
|
|
500
|
+
),
|
|
501
|
+
resets: resets.filter((key) => categoryKeys.includes(key)),
|
|
502
|
+
},
|
|
462
503
|
},
|
|
504
|
+
tx,
|
|
505
|
+
);
|
|
506
|
+
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
507
|
+
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
508
|
+
} else if (aiPlan.keepCredentialProfileIds) {
|
|
509
|
+
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
510
|
+
}
|
|
511
|
+
if (aiPlan.headerPlan) await storeAiRequestHeaderPlan(aiPlan.headerPlan, tx);
|
|
512
|
+
if (aiPlan.modelProfileIds) {
|
|
513
|
+
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, accessChanges, tx);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof settings.SettingUnreadableError) {
|
|
518
|
+
return c.json(
|
|
519
|
+
{
|
|
520
|
+
message: settingsNotSavedMessage(getLocale(c)),
|
|
521
|
+
errors: { [error.key]: unreadableSettingMessage(getLocale(c)) },
|
|
522
|
+
code: "setting_unreadable",
|
|
463
523
|
},
|
|
464
|
-
|
|
524
|
+
409,
|
|
465
525
|
);
|
|
466
|
-
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
467
|
-
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
468
|
-
} else if (aiPlan.keepCredentialProfileIds) {
|
|
469
|
-
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
470
526
|
}
|
|
471
|
-
if (
|
|
472
|
-
if (
|
|
473
|
-
|
|
527
|
+
if (error instanceof ImageMetadataError) return c.json({ message: error.message, code: error.code, errors: fieldErrors }, 422);
|
|
528
|
+
if (error instanceof AiModelAccessConflict || error instanceof AiModelAccessInvalid) {
|
|
529
|
+
return c.json(
|
|
530
|
+
{ message: error.message, errors: { [AI_PROFILES_KEY]: error.message } },
|
|
531
|
+
error instanceof AiModelAccessConflict ? 409 : 400,
|
|
532
|
+
);
|
|
474
533
|
}
|
|
475
|
-
|
|
476
|
-
} catch (error) {
|
|
477
|
-
if (error instanceof ImageMetadataError) return c.json({ message: error.message, code: error.code, errors: fieldErrors }, 422);
|
|
478
|
-
if (error instanceof AiModelAccessConflict || error instanceof AiModelAccessInvalid) {
|
|
534
|
+
console.error("[settings] failed to commit settings update", error);
|
|
479
535
|
return c.json(
|
|
480
|
-
{
|
|
481
|
-
|
|
536
|
+
{
|
|
537
|
+
message: "Save failed",
|
|
538
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: "The settings could not be saved." },
|
|
539
|
+
},
|
|
540
|
+
500,
|
|
482
541
|
);
|
|
483
542
|
}
|
|
484
|
-
console.error("[settings] failed to commit settings update", error);
|
|
485
|
-
return c.json(
|
|
486
|
-
{
|
|
487
|
-
message: "Save failed",
|
|
488
|
-
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: "The settings could not be saved." },
|
|
489
|
-
},
|
|
490
|
-
500,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
543
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
544
|
+
// Only now: dropping the cache before the commit would let another
|
|
545
|
+
// container miss, read the pre-commit row and cache it for the full TTL.
|
|
546
|
+
await invalidateCommittedSettings(keys);
|
|
497
547
|
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
return c.body(null, 204);
|
|
549
|
+
},
|
|
550
|
+
)
|
|
500
551
|
.delete("/:key{.+}", auth.requireRole("admin"), async (c) => {
|
|
501
552
|
const key = c.req.param("key");
|
|
502
553
|
if (!isKnownSetting(key)) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GEOIP_MAX_DATABASE_BYTES, GEOIP_MAX_REQUEST_BYTES } from "../contracts/geoip";
|
|
5
|
+
import { type AuthContext, auth, v } from "../server";
|
|
6
|
+
import { REQUEST_IDLE_TIMEOUT_MAX_SECONDS, REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
|
|
7
|
+
import { GeoIpError, geoip } from "../services/geoip";
|
|
8
|
+
|
|
9
|
+
export const createAdminGeoIpRoutes = (
|
|
10
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
11
|
+
service: typeof geoip = geoip,
|
|
12
|
+
) =>
|
|
13
|
+
new Hono<AuthContext>()
|
|
14
|
+
.use(authenticate)
|
|
15
|
+
.onError((error, c) => {
|
|
16
|
+
if (error instanceof GeoIpError)
|
|
17
|
+
return c.json(
|
|
18
|
+
{
|
|
19
|
+
code: error.code,
|
|
20
|
+
...(error.code === "geoip_too_large" ? { maxDatabaseBytes: GEOIP_MAX_DATABASE_BYTES } : {}),
|
|
21
|
+
...(error.code === "geoip_request_too_large" ? { maxRequestBytes: GEOIP_MAX_REQUEST_BYTES } : {}),
|
|
22
|
+
},
|
|
23
|
+
error.status,
|
|
24
|
+
);
|
|
25
|
+
if (c.req.raw.signal.aborted) return new Response(null, { status: 499 });
|
|
26
|
+
throw error;
|
|
27
|
+
})
|
|
28
|
+
.get(
|
|
29
|
+
"/",
|
|
30
|
+
describeRoute({ tags: ["Sign-in locations"], summary: "Show active sign-in location databases and upload limits" }),
|
|
31
|
+
auth.requireOAuthScope("read", "admin"),
|
|
32
|
+
async (c) => c.json(await service.status(c.get("user"))),
|
|
33
|
+
)
|
|
34
|
+
.put(
|
|
35
|
+
"/",
|
|
36
|
+
describeRoute({
|
|
37
|
+
tags: ["Sign-in locations"],
|
|
38
|
+
summary: "Replace a City or ASN database with a raw or gzip MMDB upload",
|
|
39
|
+
requestBody: {
|
|
40
|
+
required: true,
|
|
41
|
+
content: {
|
|
42
|
+
"application/octet-stream": { schema: { type: "string", format: "binary" } },
|
|
43
|
+
"application/gzip": { schema: { type: "string", format: "binary" } },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
auth.requireOAuthScope("write", "admin"),
|
|
48
|
+
async (c) => {
|
|
49
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_MAX_SECONDS);
|
|
50
|
+
try {
|
|
51
|
+
const length = c.req.header("Content-Length");
|
|
52
|
+
return c.json(
|
|
53
|
+
await service.upload(
|
|
54
|
+
c.get("user"),
|
|
55
|
+
c.req.raw.body ??
|
|
56
|
+
new ReadableStream({
|
|
57
|
+
start(controller) {
|
|
58
|
+
controller.close();
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
{
|
|
62
|
+
contentLength: length === undefined ? undefined : Number(length),
|
|
63
|
+
signal: c.req.raw.signal,
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
.delete(
|
|
73
|
+
"/:kind",
|
|
74
|
+
describeRoute({ tags: ["Sign-in locations"], summary: "Reset one sign-in database to the bundled snapshot" }),
|
|
75
|
+
auth.requireOAuthScope("write", "admin"),
|
|
76
|
+
v("param", z.object({ kind: z.enum(["city", "asn"]) })),
|
|
77
|
+
async (c) => c.json(await service.reset(c.get("user"), c.req.valid("param").kind)),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export default createAdminGeoIpRoutes();
|
|
@@ -7,6 +7,7 @@ import { listApps } from "../_internal/registry";
|
|
|
7
7
|
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
8
8
|
import { AI_INFERENCE_SCOPE } from "../contracts/shared";
|
|
9
9
|
import { type AuthContext, auth, err, respond, v } from "../server";
|
|
10
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
10
11
|
import { audit } from "../services/audit";
|
|
11
12
|
import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
|
|
12
13
|
import { WORKLOAD_SCOPES } from "../services/identity/workload-auth";
|
|
@@ -110,6 +111,8 @@ export const createAdminIdentityRoutes = (
|
|
|
110
111
|
service: IdentityAdminDependencies = dependencies,
|
|
111
112
|
) =>
|
|
112
113
|
new Hono<AuthContext>()
|
|
114
|
+
// Mandate lists hold every user's grant inputs, which users write at will and which can carry private text.
|
|
115
|
+
.use("*", noTransform())
|
|
113
116
|
.use(authenticate)
|
|
114
117
|
.use("*", async (c, next) => {
|
|
115
118
|
c.header("Cache-Control", "no-store");
|