@k2b/cloud 0.42.0 → 0.44.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 +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -10,47 +10,35 @@ import { sql } from "bun";
|
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { describeRoute } from "hono-openapi";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
+
import { ObjectStoreConfigurationError, ObjectStoreConnectionError, testObjectStoreConnection } from "../_internal/object-store-config";
|
|
13
14
|
import { listApps } from "../_internal/registry";
|
|
14
|
-
import { listAiCredentialProfileIds, pruneAiCredentials, setAiCredential, splitAiProfileCredentials } from "../ai/credentials";
|
|
15
15
|
import { enrichDirtyAiConversations } from "../ai/enrich";
|
|
16
|
-
import {
|
|
17
|
-
type AiModelAccessChange,
|
|
18
|
-
AiModelAccessConflict,
|
|
19
|
-
AiModelAccessInvalid,
|
|
20
|
-
aiModelAccess,
|
|
21
|
-
splitAiModelAccess,
|
|
22
|
-
} from "../ai/model-access";
|
|
23
|
-
import { listAiRequestHeaderNames, planAiProfileRequestHeaders, storeAiRequestHeaderPlan } from "../ai/request-headers";
|
|
24
|
-
import {
|
|
25
|
-
parseAiModelProfiles,
|
|
26
|
-
planAiProfileCredentials,
|
|
27
|
-
prepareAiModelProfileImages,
|
|
28
|
-
validateAiSettingsConfiguration,
|
|
29
|
-
} from "../ai/settings";
|
|
30
|
-
import { type AiSettingsIssueWithMessage, aiSettingsNotSavedMessage, describeAiSettingsIssues } from "../ai/settings-messages";
|
|
31
16
|
import { type AuthContext, auth, getLocale, jsonResponse, requiresAdmin, v } from "../server";
|
|
32
|
-
import { settingsDeleteLegacyKeys, settingsListLegacyKeys } from "../services";
|
|
17
|
+
import { settingsDeleteLegacyKeys, settingsListLegacyKeys, settingsService } from "../services";
|
|
33
18
|
import { readAccountCategoryPolicy } from "../services/account-category-policy";
|
|
34
19
|
import { audit } from "../services/audit";
|
|
20
|
+
import { ConfigConflict } from "../services/config-conflict";
|
|
35
21
|
import { validateFreeIpaCaCert } from "../services/freeipa-config";
|
|
36
22
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
37
23
|
import { testFreeIpaConnection } from "../services/ipa/connection";
|
|
38
24
|
import { GotenbergRenderError, testGotenberg } from "../services/pdf";
|
|
39
25
|
import * as settings from "../services/settings";
|
|
40
26
|
import { SETTINGS_MAP, validateSettingValue } from "../services/settings/defaults";
|
|
41
|
-
|
|
42
27
|
import { assertGeoBudgetPair, GeoBudgetSettingError } from "../services/settings/geo-budget";
|
|
43
28
|
import { invalidateSettingsCacheForAdmin } from "../services/settings/store";
|
|
29
|
+
import { assertSettingVersions } from "../services/settings/versions";
|
|
44
30
|
|
|
31
|
+
const BulkUpdatesSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
updates: z.record(z.string(), z.unknown()).optional().default({}),
|
|
34
|
+
resets: z.array(z.string()).optional().default([]),
|
|
35
|
+
replaceUnreadable: z.array(z.string()).optional().default([]),
|
|
36
|
+
expectedUpdatedAt: z.record(z.string(), z.string().nullable()).optional().default({}),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
45
39
|
const BulkSaveSchema = z.union([
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
updates: z.record(z.string(), z.unknown()).optional().default({}),
|
|
49
|
-
resets: z.array(z.string()).optional().default([]),
|
|
50
|
-
replaceUnreadable: z.array(z.string()).optional().default([]),
|
|
51
|
-
})
|
|
52
|
-
.strict(),
|
|
53
|
-
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[], replaceUnreadable: [] as string[] })),
|
|
40
|
+
BulkUpdatesSchema,
|
|
41
|
+
z.record(z.string(), z.unknown()).transform((updates) => BulkUpdatesSchema.parse({ updates })),
|
|
54
42
|
]);
|
|
55
43
|
const LEGAL_DOCUMENTS = [
|
|
56
44
|
{ kind: "terms", path: "/legal/terms" },
|
|
@@ -72,150 +60,8 @@ const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
|
72
60
|
const GEO_KEY = "geo.api_key";
|
|
73
61
|
const GEO_KEY_DEDICATED = "Set or remove the Geoapify key with /api/admin/core/geo/key, which verifies it first.";
|
|
74
62
|
|
|
75
|
-
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
76
|
-
const AI_ENABLED_KEY = "ai.enabled";
|
|
77
|
-
const AI_DEFAULT_MODEL_KEY = "ai.default_model_id";
|
|
78
|
-
const AI_BACKGROUND_MODEL_KEY = "ai.background_model_id";
|
|
79
|
-
const AI_AUDIO_MODEL_KEY = "ai.audio_model_id";
|
|
80
|
-
const AI_VISION_MODEL_KEY = "ai.vision_model_id";
|
|
81
|
-
const AI_WORKFLOW_MODEL_KEY = "ai.workflow_model_id";
|
|
82
|
-
const AI_CONFIGURATION_KEYS = new Set([
|
|
83
|
-
AI_ENABLED_KEY,
|
|
84
|
-
AI_DEFAULT_MODEL_KEY,
|
|
85
|
-
AI_BACKGROUND_MODEL_KEY,
|
|
86
|
-
AI_VISION_MODEL_KEY,
|
|
87
|
-
AI_AUDIO_MODEL_KEY,
|
|
88
|
-
AI_WORKFLOW_MODEL_KEY,
|
|
89
|
-
AI_PROFILES_KEY,
|
|
90
|
-
]);
|
|
91
63
|
const INTEGER_FREEIPA_SETTINGS = new Set(["freeipa.sync_guard.max_user_changes", "freeipa.sync_guard.max_group_deletions"]);
|
|
92
64
|
|
|
93
|
-
/**
|
|
94
|
-
* Move provider keys out of the model-profiles value before it is stored.
|
|
95
|
-
*
|
|
96
|
-
* The admin form posts profiles as one value, and a newly typed key rides along
|
|
97
|
-
* on its profile. Keys must not land in the setting — it is a `text` setting and
|
|
98
|
-
* would be handed back to the browser in full — so they go to
|
|
99
|
-
* `ai.model_credentials` here and are stripped from what gets saved. A profile
|
|
100
|
-
* whose key field was left empty keeps whatever is already stored.
|
|
101
|
-
*
|
|
102
|
-
* Doing this in the route rather than the settings service keeps the generic
|
|
103
|
-
* store free of AI knowledge, and keeps the admin page at a single save request.
|
|
104
|
-
*/
|
|
105
|
-
const storeAiCredentials = async (
|
|
106
|
-
split: NonNullable<ReturnType<typeof splitAiProfileCredentials>>,
|
|
107
|
-
keepCredentialProfileIds: readonly string[],
|
|
108
|
-
db: typeof sql,
|
|
109
|
-
): Promise<void> => {
|
|
110
|
-
for (const { profileId, secret } of split.credentials) await setAiCredential(profileId, secret, db);
|
|
111
|
-
await pruneAiCredentials(keepCredentialProfileIds, db);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
type AiSettingsMutationPlan = {
|
|
115
|
-
errors: FieldErrors;
|
|
116
|
-
/** Structured validation issues with stable codes; `errors` carries the same messages keyed by setting. */
|
|
117
|
-
issues?: AiSettingsIssueWithMessage[];
|
|
118
|
-
keepCredentialProfileIds?: string[];
|
|
119
|
-
headerPlan?: ReturnType<typeof planAiProfileRequestHeaders>;
|
|
120
|
-
modelProfileIds?: string[];
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const valueAfterMutation = <T>(key: string, current: T, updates: Record<string, unknown>, resets: readonly string[]): T => {
|
|
124
|
-
if (key in updates) return updates[key] as T;
|
|
125
|
-
if (resets.includes(key)) return SETTINGS_MAP.get(key)?.default as T;
|
|
126
|
-
return current;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const prepareAiSettingsMutation = async (
|
|
130
|
-
updates: Record<string, unknown>,
|
|
131
|
-
resets: readonly string[],
|
|
132
|
-
aiSplit: ReturnType<typeof splitAiProfileCredentials>,
|
|
133
|
-
locale: string,
|
|
134
|
-
): Promise<AiSettingsMutationPlan> => {
|
|
135
|
-
const keys = [...Object.keys(updates), ...resets];
|
|
136
|
-
if (!keys.some((key) => AI_CONFIGURATION_KEYS.has(key))) return { errors: {} };
|
|
137
|
-
|
|
138
|
-
const [
|
|
139
|
-
currentEnabled,
|
|
140
|
-
currentDefaultModelId,
|
|
141
|
-
currentBackgroundModelId,
|
|
142
|
-
currentVisionModelId,
|
|
143
|
-
currentAudioModelId,
|
|
144
|
-
currentWorkflowModelId,
|
|
145
|
-
currentProfilesJson,
|
|
146
|
-
] = await Promise.all([
|
|
147
|
-
settings.get<boolean>(AI_ENABLED_KEY),
|
|
148
|
-
settings.get<string>(AI_DEFAULT_MODEL_KEY),
|
|
149
|
-
settings.get<string>(AI_BACKGROUND_MODEL_KEY),
|
|
150
|
-
settings.get<string>(AI_VISION_MODEL_KEY),
|
|
151
|
-
settings.get<string>(AI_AUDIO_MODEL_KEY),
|
|
152
|
-
settings.get<string>(AI_WORKFLOW_MODEL_KEY),
|
|
153
|
-
settings.get<string>(AI_PROFILES_KEY),
|
|
154
|
-
]);
|
|
155
|
-
|
|
156
|
-
const currentParsed = parseAiModelProfiles(currentProfilesJson ?? "[]");
|
|
157
|
-
const profilesUpdated = AI_PROFILES_KEY in updates;
|
|
158
|
-
const profilesReset = resets.includes(AI_PROFILES_KEY);
|
|
159
|
-
const nextParsed: ReturnType<typeof parseAiModelProfiles> = profilesUpdated
|
|
160
|
-
? parseAiModelProfiles(aiSplit?.profilesJson ?? "[]")
|
|
161
|
-
: profilesReset
|
|
162
|
-
? { profiles: [] }
|
|
163
|
-
: currentParsed;
|
|
164
|
-
if (nextParsed.error) return { errors: nextParsed.error.fields ?? { [AI_PROFILES_KEY]: nextParsed.error.message } };
|
|
165
|
-
|
|
166
|
-
const nextEnabled = Boolean(valueAfterMutation(AI_ENABLED_KEY, currentEnabled, updates, resets));
|
|
167
|
-
const existingCredentialProfileIds = profilesUpdated || (!profilesReset && nextEnabled) ? await listAiCredentialProfileIds() : [];
|
|
168
|
-
|
|
169
|
-
let keepCredentialProfileIds: string[] | undefined;
|
|
170
|
-
if (profilesUpdated || profilesReset) {
|
|
171
|
-
const credentialPlan = planAiProfileCredentials({
|
|
172
|
-
currentProfiles: currentParsed.profiles,
|
|
173
|
-
nextProfiles: nextParsed.profiles,
|
|
174
|
-
existingCredentialProfileIds,
|
|
175
|
-
submittedCredentialProfileIds: aiSplit?.credentials.map(({ profileId }) => profileId) ?? [],
|
|
176
|
-
});
|
|
177
|
-
if (credentialPlan.unsupportedCredentialProfileId) {
|
|
178
|
-
return {
|
|
179
|
-
errors: {
|
|
180
|
-
[AI_PROFILES_KEY]: `Profile "${credentialPlan.unsupportedCredentialProfileId}" does not support provider credentials.`,
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
keepCredentialProfileIds = credentialPlan.keepCredentialProfileIds;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const headerPlan =
|
|
188
|
-
profilesUpdated || profilesReset
|
|
189
|
-
? planAiProfileRequestHeaders({
|
|
190
|
-
currentProfiles: currentParsed.profiles,
|
|
191
|
-
nextProfiles: nextParsed.profiles,
|
|
192
|
-
existingNames: profilesUpdated ? await listAiRequestHeaderNames() : {},
|
|
193
|
-
submitted: aiSplit?.requestHeaders ?? [],
|
|
194
|
-
})
|
|
195
|
-
: undefined;
|
|
196
|
-
if (headerPlan?.error) return { errors: { [AI_PROFILES_KEY]: headerPlan.error } };
|
|
197
|
-
|
|
198
|
-
const issues = validateAiSettingsConfiguration({
|
|
199
|
-
enabled: nextEnabled,
|
|
200
|
-
defaultModelId: String(valueAfterMutation(AI_DEFAULT_MODEL_KEY, currentDefaultModelId ?? "", updates, resets)),
|
|
201
|
-
backgroundModelId: String(valueAfterMutation(AI_BACKGROUND_MODEL_KEY, currentBackgroundModelId ?? "", updates, resets)),
|
|
202
|
-
audioModelId: String(valueAfterMutation(AI_AUDIO_MODEL_KEY, currentAudioModelId ?? "", updates, resets)),
|
|
203
|
-
visionModelId: String(valueAfterMutation(AI_VISION_MODEL_KEY, currentVisionModelId ?? "", updates, resets)),
|
|
204
|
-
workflowModelId: String(valueAfterMutation(AI_WORKFLOW_MODEL_KEY, currentWorkflowModelId ?? "", updates, resets)),
|
|
205
|
-
profiles: nextParsed.profiles,
|
|
206
|
-
credentialProfileIds: keepCredentialProfileIds ?? existingCredentialProfileIds,
|
|
207
|
-
});
|
|
208
|
-
// Stored profiles come second so a profile that is being removed keeps its label.
|
|
209
|
-
const described = describeAiSettingsIssues(issues, { locale, profiles: [...nextParsed.profiles, ...currentParsed.profiles] });
|
|
210
|
-
return {
|
|
211
|
-
errors: described.errors,
|
|
212
|
-
issues: described.issues,
|
|
213
|
-
headerPlan,
|
|
214
|
-
keepCredentialProfileIds,
|
|
215
|
-
modelProfileIds: profilesUpdated || profilesReset ? nextParsed.profiles.map((profile) => profile.id) : undefined,
|
|
216
|
-
};
|
|
217
|
-
};
|
|
218
|
-
|
|
219
65
|
const invalidateCommittedSettings = async (keys: readonly string[]): Promise<void> => {
|
|
220
66
|
try {
|
|
221
67
|
await settings.invalidateSettingsCache(keys);
|
|
@@ -316,6 +162,22 @@ const app = new Hono<AuthContext>()
|
|
|
316
162
|
);
|
|
317
163
|
return c.json({ items });
|
|
318
164
|
})
|
|
165
|
+
// The current values and versions of one settings group, so a form can continue after a save without a reload.
|
|
166
|
+
.get(
|
|
167
|
+
"/entries",
|
|
168
|
+
auth.requireRole("admin"),
|
|
169
|
+
describeRoute({
|
|
170
|
+
tags: ["Administration"],
|
|
171
|
+
summary: "Read the settings of one group with their versions",
|
|
172
|
+
...requiresAdmin,
|
|
173
|
+
}),
|
|
174
|
+
v("query", z.object({ group: z.string().min(1).max(64) })),
|
|
175
|
+
async (c) => {
|
|
176
|
+
const result = await settingsService.entry.list({ filter: { group: c.req.valid("query").group }, locale: getLocale(c) });
|
|
177
|
+
c.header("Cache-Control", "no-store");
|
|
178
|
+
return c.json({ items: result.items });
|
|
179
|
+
},
|
|
180
|
+
)
|
|
319
181
|
.get("/legacy", auth.requireRole("admin"), async (c) => {
|
|
320
182
|
return c.json(await settingsListLegacyKeys(await liveSettingKeys()));
|
|
321
183
|
})
|
|
@@ -345,6 +207,33 @@ const app = new Hono<AuthContext>()
|
|
|
345
207
|
return c.json({ message: "Failed to test Gotenberg PDF rendering" }, 500);
|
|
346
208
|
}
|
|
347
209
|
})
|
|
210
|
+
.post(
|
|
211
|
+
"/test-storage",
|
|
212
|
+
auth.requireRole("admin"),
|
|
213
|
+
describeRoute({
|
|
214
|
+
tags: ["Administration"],
|
|
215
|
+
summary: "Test the saved Garage storage connection",
|
|
216
|
+
...requiresAdmin,
|
|
217
|
+
responses: { 200: jsonResponse(z.object({ ok: z.literal(true) }), "Object written, read and deleted") },
|
|
218
|
+
}),
|
|
219
|
+
async (c) => {
|
|
220
|
+
c.header("Cache-Control", "no-store");
|
|
221
|
+
try {
|
|
222
|
+
await testObjectStoreConnection();
|
|
223
|
+
return c.json({ ok: true as const });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return c.json(
|
|
226
|
+
{
|
|
227
|
+
message:
|
|
228
|
+
error instanceof ObjectStoreConfigurationError || error instanceof ObjectStoreConnectionError
|
|
229
|
+
? error.message
|
|
230
|
+
: "Storage test failed. Check the saved settings, Garage availability and bucket permissions, then retry.",
|
|
231
|
+
},
|
|
232
|
+
502,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
)
|
|
348
237
|
.post("/test-freeipa", auth.requireRole("admin"), async (c) => {
|
|
349
238
|
const result = await testFreeIpaConnection();
|
|
350
239
|
if (result.ok) return c.json({ ok: true });
|
|
@@ -368,12 +257,12 @@ const app = new Hono<AuthContext>()
|
|
|
368
257
|
}),
|
|
369
258
|
v("json", BulkSaveSchema),
|
|
370
259
|
async (c) => {
|
|
371
|
-
const { updates, resets, replaceUnreadable } = c.req.valid("json");
|
|
260
|
+
const { updates, resets, replaceUnreadable, expectedUpdatedAt } = c.req.valid("json");
|
|
372
261
|
const replace = new Set(replaceUnreadable);
|
|
373
262
|
const updateKeys = Object.keys(updates);
|
|
374
263
|
const keys = [...updateKeys, ...resets];
|
|
375
264
|
|
|
376
|
-
if (keys.length === 0 && replace.size === 0) {
|
|
265
|
+
if (keys.length === 0 && replace.size === 0 && Object.keys(expectedUpdatedAt).length === 0) {
|
|
377
266
|
return c.body(null, 204);
|
|
378
267
|
}
|
|
379
268
|
|
|
@@ -383,6 +272,9 @@ const app = new Hono<AuthContext>()
|
|
|
383
272
|
ownership[key] = `Unknown setting "${key}"`;
|
|
384
273
|
}
|
|
385
274
|
}
|
|
275
|
+
for (const key of Object.keys(expectedUpdatedAt)) {
|
|
276
|
+
if (!keys.includes(key)) ownership[key] = `Setting "${key}" must be included in updates or resets`;
|
|
277
|
+
}
|
|
386
278
|
for (const key of replace) {
|
|
387
279
|
if (!Object.hasOwn(updates, key)) ownership[key] = `Setting "${key}" must be included in updates to replace its unreadable value`;
|
|
388
280
|
}
|
|
@@ -405,38 +297,7 @@ const app = new Hono<AuthContext>()
|
|
|
405
297
|
return c.json({ message: settingsNotSavedMessage(getLocale(c)), errors: unreadable, code: "setting_unreadable" }, 409);
|
|
406
298
|
}
|
|
407
299
|
|
|
408
|
-
|
|
409
|
-
// value that gets checked is the value that gets stored.
|
|
410
|
-
const aiSplit = AI_PROFILES_KEY in updates ? splitAiProfileCredentials(updates[AI_PROFILES_KEY]) : null;
|
|
411
|
-
if (AI_PROFILES_KEY in updates && !aiSplit) {
|
|
412
|
-
// Unsplittable means the keys could not be separated out, and storing the
|
|
413
|
-
// value as posted would put them back in a setting the browser reads.
|
|
414
|
-
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
415
|
-
}
|
|
416
|
-
let accessChanges: AiModelAccessChange[] = [];
|
|
417
|
-
if (aiSplit) {
|
|
418
|
-
try {
|
|
419
|
-
aiSplit.profilesJson = prepareAiModelProfileImages(aiSplit.profilesJson);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
if (error instanceof ImageMetadataError)
|
|
422
|
-
return c.json({ message: error.message, code: error.code, errors: { [AI_PROFILES_KEY]: error.message } }, 422);
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
const split = splitAiModelAccess(aiSplit.profilesJson);
|
|
427
|
-
aiSplit.profilesJson = split.profilesJson;
|
|
428
|
-
accessChanges = split.changes;
|
|
429
|
-
} catch {
|
|
430
|
-
return c.json(
|
|
431
|
-
{
|
|
432
|
-
message: "Invalid model permissions",
|
|
433
|
-
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
434
|
-
},
|
|
435
|
-
400,
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
300
|
+
const finalValues = updates;
|
|
440
301
|
|
|
441
302
|
// Validate everything up front. settings.set validates as it writes, so a
|
|
442
303
|
// single bad field used to leave the earlier keys already saved — with the
|
|
@@ -461,26 +322,17 @@ const app = new Hono<AuthContext>()
|
|
|
461
322
|
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
462
323
|
}
|
|
463
324
|
|
|
464
|
-
let aiPlan: AiSettingsMutationPlan;
|
|
465
|
-
try {
|
|
466
|
-
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit, getLocale(c));
|
|
467
|
-
} catch (error) {
|
|
468
|
-
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
469
|
-
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
470
|
-
}
|
|
471
|
-
if (Object.keys(aiPlan.errors).length > 0) {
|
|
472
|
-
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
325
|
const fieldErrors: FieldErrors = {};
|
|
476
326
|
try {
|
|
477
327
|
// Every settings and AI credential write runs on the transaction's own
|
|
478
328
|
// connection, so a failure rolls the whole save back.
|
|
479
329
|
await sql.begin(async (tx) => {
|
|
330
|
+
await assertSettingVersions(expectedUpdatedAt, tx);
|
|
480
331
|
await assertGeoBudgetPair(tx, validatedValues, resets);
|
|
481
332
|
for (const [key, value] of Object.entries(validatedValues)) {
|
|
482
333
|
try {
|
|
483
334
|
await settings.set(key, value, tx, { replaceUnreadable: replace.has(key) });
|
|
335
|
+
await tx`UPDATE settings.entries SET updated_by=${c.get("user").id}::uuid WHERE key=${key}`;
|
|
484
336
|
} catch (error) {
|
|
485
337
|
fieldErrors[key] = `Failed to save ${key}`;
|
|
486
338
|
throw error;
|
|
@@ -512,15 +364,6 @@ const app = new Hono<AuthContext>()
|
|
|
512
364
|
},
|
|
513
365
|
tx,
|
|
514
366
|
);
|
|
515
|
-
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
516
|
-
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
517
|
-
} else if (aiPlan.keepCredentialProfileIds) {
|
|
518
|
-
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
519
|
-
}
|
|
520
|
-
if (aiPlan.headerPlan) await storeAiRequestHeaderPlan(aiPlan.headerPlan, tx);
|
|
521
|
-
if (aiPlan.modelProfileIds) {
|
|
522
|
-
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, accessChanges, tx);
|
|
523
|
-
}
|
|
524
367
|
});
|
|
525
368
|
} catch (error) {
|
|
526
369
|
if (error instanceof GeoBudgetSettingError) return error.getResponse();
|
|
@@ -535,12 +378,11 @@ const app = new Hono<AuthContext>()
|
|
|
535
378
|
);
|
|
536
379
|
}
|
|
537
380
|
if (error instanceof ImageMetadataError) return c.json({ message: error.message, code: error.code, errors: fieldErrors }, 422);
|
|
538
|
-
if (error instanceof
|
|
381
|
+
if (error instanceof ConfigConflict)
|
|
539
382
|
return c.json(
|
|
540
|
-
{
|
|
541
|
-
|
|
383
|
+
{ code: error.code, current: error.current, key: error.key, changedBy: error.changedBy, changedAt: error.changedAt },
|
|
384
|
+
409,
|
|
542
385
|
);
|
|
543
|
-
}
|
|
544
386
|
console.error("[settings] failed to commit settings update", error);
|
|
545
387
|
return c.json(
|
|
546
388
|
{
|
|
@@ -565,17 +407,6 @@ const app = new Hono<AuthContext>()
|
|
|
565
407
|
}
|
|
566
408
|
if (key === GEO_KEY) return c.json({ message: GEO_KEY_DEDICATED, errors: { [GEO_KEY]: GEO_KEY_DEDICATED } }, 400);
|
|
567
409
|
|
|
568
|
-
let aiPlan: AiSettingsMutationPlan;
|
|
569
|
-
try {
|
|
570
|
-
aiPlan = await prepareAiSettingsMutation({}, [key], null, getLocale(c));
|
|
571
|
-
} catch (error) {
|
|
572
|
-
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
573
|
-
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
574
|
-
}
|
|
575
|
-
if (Object.keys(aiPlan.errors).length > 0) {
|
|
576
|
-
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
410
|
try {
|
|
580
411
|
await sql.begin(async (tx) => {
|
|
581
412
|
await assertGeoBudgetPair(tx, {}, [key]);
|
|
@@ -591,13 +422,6 @@ const app = new Hono<AuthContext>()
|
|
|
591
422
|
},
|
|
592
423
|
tx,
|
|
593
424
|
);
|
|
594
|
-
if (aiPlan.keepCredentialProfileIds) {
|
|
595
|
-
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
596
|
-
}
|
|
597
|
-
if (aiPlan.headerPlan) await storeAiRequestHeaderPlan(aiPlan.headerPlan, tx);
|
|
598
|
-
if (aiPlan.modelProfileIds) {
|
|
599
|
-
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, [], tx);
|
|
600
|
-
}
|
|
601
425
|
});
|
|
602
426
|
} catch (error) {
|
|
603
427
|
if (error instanceof GeoBudgetSettingError || error instanceof settings.SettingUnreadableError) return error.getResponse();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { PaginationQuerySchema, parsePagination } from "../contracts";
|
|
5
|
+
import { type AuthContext, auth, expectUserBackedActor, requiresAdmin, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
7
|
+
import { DirectorySharePrincipalSchema, directoryShares } from "../services/accounts/directory-shares";
|
|
8
|
+
|
|
9
|
+
const Recipient = z.object({ recipientId: z.uuid() });
|
|
10
|
+
const Share = Recipient.extend({ id: z.uuid() });
|
|
11
|
+
const routes = new Hono<AuthContext>()
|
|
12
|
+
.use("*", noTransform(), auth.requireRole("admin"))
|
|
13
|
+
.get(
|
|
14
|
+
"/:recipientId",
|
|
15
|
+
describeRoute({ ...requiresAdmin, tags: ["Service accounts"], summary: "List exact directory entries shared with a machine account" }),
|
|
16
|
+
v("param", Recipient),
|
|
17
|
+
v("query", PaginationQuerySchema),
|
|
18
|
+
async (c) => {
|
|
19
|
+
const pagination = parsePagination(c.req.valid("query"));
|
|
20
|
+
return respond(
|
|
21
|
+
c,
|
|
22
|
+
directoryShares.list({
|
|
23
|
+
actor: expectUserBackedActor(c),
|
|
24
|
+
recipientId: c.req.valid("param").recipientId,
|
|
25
|
+
page: pagination.page,
|
|
26
|
+
perPage: pagination.perPage,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
.post(
|
|
32
|
+
"/:recipientId",
|
|
33
|
+
auth.requireOAuthScope("write", "admin"),
|
|
34
|
+
describeRoute({ ...requiresAdmin, tags: ["Service accounts"], summary: "Share one directory entry with a machine account" }),
|
|
35
|
+
v("param", Recipient),
|
|
36
|
+
v("json", z.object({ principal: DirectorySharePrincipalSchema }).strict()),
|
|
37
|
+
(c) =>
|
|
38
|
+
respond(
|
|
39
|
+
c,
|
|
40
|
+
directoryShares.grant({
|
|
41
|
+
actor: expectUserBackedActor(c),
|
|
42
|
+
recipientId: c.req.valid("param").recipientId,
|
|
43
|
+
principal: c.req.valid("json").principal,
|
|
44
|
+
}),
|
|
45
|
+
201,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
.delete(
|
|
49
|
+
"/:recipientId/:id",
|
|
50
|
+
auth.requireOAuthScope("write", "admin"),
|
|
51
|
+
describeRoute({ ...requiresAdmin, tags: ["Service accounts"], summary: "Revoke a machine account's directory share" }),
|
|
52
|
+
v("param", Share),
|
|
53
|
+
(c) => respond(c, directoryShares.revoke({ actor: expectUserBackedActor(c), ...c.req.valid("param") })),
|
|
54
|
+
);
|
|
55
|
+
export default routes;
|
|
@@ -5,9 +5,11 @@ 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 { SaveServiceAccountAvatarSchema } from "../contracts/profile";
|
|
8
9
|
import { AI_INFERENCE_SCOPE } from "../contracts/shared";
|
|
9
10
|
import { type AuthContext, auth, err, respond, v } from "../server";
|
|
10
11
|
import { noTransform } from "../server/middleware/no-transform";
|
|
12
|
+
import { clearServiceAccountAvatar, getServiceAccountAvatarState, updateServiceAccountAvatar } from "../services/accounts/avatar";
|
|
11
13
|
import { audit } from "../services/audit";
|
|
12
14
|
import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
|
|
13
15
|
import { WORKLOAD_SCOPES } from "../services/identity/workload-auth";
|
|
@@ -65,14 +67,19 @@ const ServiceAccountStatusInputSchema = z.object({ status: z.enum(["active", "di
|
|
|
65
67
|
const ServiceAccountApiKeyInputSchema = z
|
|
66
68
|
.object({
|
|
67
69
|
name: z.string().trim().min(1).max(120),
|
|
70
|
+
access: z.enum(["full", "models"]).default("full"),
|
|
68
71
|
scopes: z
|
|
69
72
|
.array(z.string().trim().min(1).max(64))
|
|
70
73
|
.max(20)
|
|
71
74
|
.default([])
|
|
72
|
-
.refine((scopes) => !scopes.includes(AI_INFERENCE_SCOPE), "
|
|
75
|
+
.refine((scopes) => !scopes.includes(AI_INFERENCE_SCOPE), "Use access: models to create a models-only key"),
|
|
73
76
|
expiresAt: z.iso.datetime().nullable().optional(),
|
|
74
77
|
})
|
|
75
|
-
.strict()
|
|
78
|
+
.strict()
|
|
79
|
+
.refine(
|
|
80
|
+
(input) => input.access !== "models" || input.scopes.length === 0,
|
|
81
|
+
"Models-only access cannot be combined with general credential scopes",
|
|
82
|
+
);
|
|
76
83
|
|
|
77
84
|
type IdentityAdminDependencies = {
|
|
78
85
|
apps: () => Promise<Array<{ id: string; name: string }>>;
|
|
@@ -86,6 +93,11 @@ type IdentityAdminDependencies = {
|
|
|
86
93
|
>;
|
|
87
94
|
mandates: Pick<typeof mandates, "list">;
|
|
88
95
|
audit: Pick<typeof audit, "record">;
|
|
96
|
+
avatars: {
|
|
97
|
+
state: typeof getServiceAccountAvatarState;
|
|
98
|
+
update: typeof updateServiceAccountAvatar;
|
|
99
|
+
clear: typeof clearServiceAccountAvatar;
|
|
100
|
+
};
|
|
89
101
|
};
|
|
90
102
|
|
|
91
103
|
const dependencies: IdentityAdminDependencies = {
|
|
@@ -97,6 +109,7 @@ const dependencies: IdentityAdminDependencies = {
|
|
|
97
109
|
credentials: serviceAccountCredentials,
|
|
98
110
|
mandates,
|
|
99
111
|
audit,
|
|
112
|
+
avatars: { state: getServiceAccountAvatarState, update: updateServiceAccountAvatar, clear: clearServiceAccountAvatar },
|
|
100
113
|
};
|
|
101
114
|
|
|
102
115
|
const auditActor = (user: AuthContext["Variables"]["user"]) => ({
|
|
@@ -114,6 +127,9 @@ export const createAdminIdentityRoutes = (
|
|
|
114
127
|
// Mandate lists hold every user's grant inputs, which users write at will and which can carry private text.
|
|
115
128
|
.use("*", noTransform())
|
|
116
129
|
.use(authenticate)
|
|
130
|
+
.use("*", (c, next) =>
|
|
131
|
+
["POST", "PUT", "PATCH", "DELETE"].includes(c.req.method) ? auth.requireOAuthScope("write", "admin")(c, next) : next(),
|
|
132
|
+
)
|
|
117
133
|
.use("*", async (c, next) => {
|
|
118
134
|
c.header("Cache-Control", "no-store");
|
|
119
135
|
await next();
|
|
@@ -276,6 +292,69 @@ export const createAdminIdentityRoutes = (
|
|
|
276
292
|
return ok({ ...account, status });
|
|
277
293
|
}),
|
|
278
294
|
)
|
|
295
|
+
// Every service account shows a robot drawn from its id until someone customises it or sets a photo.
|
|
296
|
+
.get(
|
|
297
|
+
"/service-accounts/:id/avatar",
|
|
298
|
+
describeRoute({ tags: ["Service accounts"], summary: "Read a standalone service account's picture: its robot or photo" }),
|
|
299
|
+
v("param", ServiceAccountParamsSchema),
|
|
300
|
+
async (c) =>
|
|
301
|
+
respond(c, async () => {
|
|
302
|
+
const { id } = c.req.valid("param");
|
|
303
|
+
const account = await service.serviceAccounts.get({ id });
|
|
304
|
+
if (!account || !isStandaloneServiceAccountKind(account.kind)) return fail(err.notFound("Service account"));
|
|
305
|
+
const state = await service.avatars.state({ id });
|
|
306
|
+
return state ? ok(state) : fail(err.notFound("Service account"));
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
.put(
|
|
310
|
+
"/service-accounts/:id/avatar",
|
|
311
|
+
describeRoute({
|
|
312
|
+
tags: ["Service accounts"],
|
|
313
|
+
summary: "Set a standalone service account's picture: a photo, a customised robot, or which of them shows",
|
|
314
|
+
}),
|
|
315
|
+
v("param", ServiceAccountParamsSchema),
|
|
316
|
+
v("json", SaveServiceAccountAvatarSchema),
|
|
317
|
+
async (c) =>
|
|
318
|
+
respond(c, async () => {
|
|
319
|
+
const { id } = c.req.valid("param");
|
|
320
|
+
const actor = c.get("user");
|
|
321
|
+
const account = await service.serviceAccounts.get({ id });
|
|
322
|
+
if (!account || !isStandaloneServiceAccountKind(account.kind)) return fail(err.notFound("Service account"));
|
|
323
|
+
const updated = await service.avatars.update({ id, change: c.req.valid("json") });
|
|
324
|
+
if (!updated.ok)
|
|
325
|
+
return fail(updated.status === 409 ? { ...err.conflict("Picture"), message: updated.error } : err.badInput(updated.error));
|
|
326
|
+
await service.audit.record({
|
|
327
|
+
action: "service_account.avatar.update",
|
|
328
|
+
outcome: "allowed",
|
|
329
|
+
actor: auditActor(actor),
|
|
330
|
+
target: { type: "service_account", id: account.id, label: account.name },
|
|
331
|
+
metadata: { serviceAccountId: account.id, serviceAccountKind: account.kind, kind: updated.data.kind },
|
|
332
|
+
});
|
|
333
|
+
return ok(updated.data);
|
|
334
|
+
}),
|
|
335
|
+
)
|
|
336
|
+
.delete(
|
|
337
|
+
"/service-accounts/:id/avatar",
|
|
338
|
+
describeRoute({ tags: ["Service accounts"], summary: "Delete a standalone service account's photo; its robot shows again" }),
|
|
339
|
+
v("param", ServiceAccountParamsSchema),
|
|
340
|
+
async (c) =>
|
|
341
|
+
respond(c, async () => {
|
|
342
|
+
const { id } = c.req.valid("param");
|
|
343
|
+
const actor = c.get("user");
|
|
344
|
+
const account = await service.serviceAccounts.get({ id });
|
|
345
|
+
if (!account || !isStandaloneServiceAccountKind(account.kind)) return fail(err.notFound("Service account"));
|
|
346
|
+
const cleared = await service.avatars.clear({ id });
|
|
347
|
+
if (!cleared.ok) return fail(err.notFound("Service account"));
|
|
348
|
+
await service.audit.record({
|
|
349
|
+
action: "service_account.avatar.clear",
|
|
350
|
+
outcome: "allowed",
|
|
351
|
+
actor: auditActor(actor),
|
|
352
|
+
target: { type: "service_account", id: account.id, label: account.name },
|
|
353
|
+
metadata: { serviceAccountId: account.id, serviceAccountKind: account.kind },
|
|
354
|
+
});
|
|
355
|
+
return ok(cleared.data);
|
|
356
|
+
}),
|
|
357
|
+
)
|
|
279
358
|
.post(
|
|
280
359
|
"/service-accounts/:id/api-keys",
|
|
281
360
|
describeRoute({ tags: ["Service accounts"], summary: "Create an API key for a standalone service account; return its token once" }),
|