@k2b/cloud 0.6.0 → 0.8.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 +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { registerHelp } from "../services/help";
|
|
2
|
+
import { preloadLayoutHelp } from "../ssr/help";
|
|
3
|
+
import { invokeCapabilityStream } from "./capability-streams";
|
|
4
|
+
import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
|
|
1
5
|
/**
|
|
2
6
|
* defineApp() — The single entry point for every cloud app.
|
|
3
7
|
*
|
|
@@ -5,6 +9,8 @@
|
|
|
5
9
|
* Returns `{ ssr, plugin, config, meta, start }`.
|
|
6
10
|
*/
|
|
7
11
|
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
8
14
|
import type { SsrConfig } from "@k2b/ssr";
|
|
9
15
|
import { createConfig as createSsrConfig } from "@k2b/ssr";
|
|
10
16
|
import { routes } from "@k2b/ssr/hono";
|
|
@@ -18,10 +24,17 @@ import type {
|
|
|
18
24
|
AppLifecycle,
|
|
19
25
|
AppMeta,
|
|
20
26
|
AppPresentationCatalog,
|
|
27
|
+
AppSearchLink,
|
|
21
28
|
CloudLifecycleContext,
|
|
22
29
|
WidgetEndpoint,
|
|
23
30
|
} from "../contracts/app";
|
|
24
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
33
|
+
CAPABILITY_MAX_REQUEST_BYTES,
|
|
34
|
+
CAPABILITY_ORIGIN_HEADER,
|
|
35
|
+
type CapabilityDefinitions,
|
|
36
|
+
CapabilityOriginSchema,
|
|
37
|
+
} from "../contracts/capabilities";
|
|
25
38
|
import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
|
|
26
39
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
27
40
|
import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
@@ -30,18 +43,19 @@ import type { HelpDefinition } from "../server/help";
|
|
|
30
43
|
import { getLocale, resolveLocale } from "../server/locale";
|
|
31
44
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
32
45
|
import { requireInvocation } from "../server/middleware/invocation";
|
|
33
|
-
import { routeTemplate } from "../server/middleware/route-template";
|
|
46
|
+
import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
|
|
34
47
|
import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
|
|
35
|
-
import { settings as settingsMiddleware } from "../server/middleware/settings";
|
|
48
|
+
import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
|
|
36
49
|
import {
|
|
37
50
|
capabilityInvocationOperation,
|
|
38
51
|
searchInvocationOperation,
|
|
39
52
|
syncInvocationOperation,
|
|
40
53
|
widgetInvocationOperation,
|
|
41
54
|
} from "../services/identity/invocation-operations";
|
|
55
|
+
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
42
56
|
import { logger } from "../services/logging";
|
|
43
57
|
import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
|
|
44
|
-
import {
|
|
58
|
+
import { readRailSnapshot } from "../services/rail-snapshot";
|
|
45
59
|
import { get, loadCache as loadSettingsCache, set } from "../services/settings";
|
|
46
60
|
import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
|
|
47
61
|
import { registerSettings, toLegacySettingDefs } from "../services/settings/defaults";
|
|
@@ -58,12 +72,14 @@ import { compileCapabilities, invokeCompiledCapability, reviewCompiledCapability
|
|
|
58
72
|
import { createHeartbeat } from "./heartbeat";
|
|
59
73
|
import { compileHelp } from "./help";
|
|
60
74
|
import { createPageResponses } from "./page-responses";
|
|
75
|
+
import { configurePostgresApplicationName } from "./postgres-application-name";
|
|
61
76
|
import { getProcessSync, startProcessSync } from "./process-sync";
|
|
62
77
|
import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
|
|
63
|
-
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry
|
|
78
|
+
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
|
|
64
79
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
65
80
|
import { servePublicAsset } from "./static-assets";
|
|
66
81
|
import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
|
|
82
|
+
import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
|
|
67
83
|
|
|
68
84
|
/** Cache-busting version stamp — changes on every server start / rebuild. */
|
|
69
85
|
const v = Date.now();
|
|
@@ -76,6 +92,8 @@ export type PageOptions = {
|
|
|
76
92
|
lang?: string;
|
|
77
93
|
};
|
|
78
94
|
|
|
95
|
+
type ResolvedPageOptions = PageOptions & { performanceRoute?: string };
|
|
96
|
+
|
|
79
97
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
80
98
|
|
|
81
99
|
/**
|
|
@@ -103,6 +121,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
103
121
|
adminHref?: string;
|
|
104
122
|
/** Multi-link admin navigation contributed by this app. */
|
|
105
123
|
adminNav?: ReadonlyArray<{
|
|
124
|
+
section?: "ai";
|
|
106
125
|
id?: string;
|
|
107
126
|
label: string;
|
|
108
127
|
links: ReadonlyArray<{ label: string; href: string; icon: string }>;
|
|
@@ -136,6 +155,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
136
155
|
* always open in a new tab from the login footer.
|
|
137
156
|
*/
|
|
138
157
|
legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
|
|
158
|
+
/** Static destinations for the global search UI; no capabilities are created. */
|
|
159
|
+
searchLinks?: readonly AppSearchLink[];
|
|
139
160
|
/**
|
|
140
161
|
* Dashboard widget endpoints this app exposes. Each entry references an
|
|
141
162
|
* HTTP path on this app that returns a `WidgetResponse`. The dashboard
|
|
@@ -177,9 +198,10 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
177
198
|
*/
|
|
178
199
|
openapi?: string;
|
|
179
200
|
/**
|
|
180
|
-
* Project root used
|
|
201
|
+
* Project root used for SSR component IDs and development assets.
|
|
181
202
|
* Defaults to `process.cwd()`. Override only if you run the entrypoint
|
|
182
|
-
* from a directory other than the project root.
|
|
203
|
+
* from a directory other than the project root. Discovery uses APP_DIR/src
|
|
204
|
+
* (or appRoot/src) plus the installed Cloud framework source.
|
|
183
205
|
*/
|
|
184
206
|
appRoot?: string;
|
|
185
207
|
};
|
|
@@ -280,7 +302,8 @@ export const defineApp = <
|
|
|
280
302
|
>(
|
|
281
303
|
opts: AppOptions<S, N, AppId>,
|
|
282
304
|
): AppDefinition<S, N, AppId> => {
|
|
283
|
-
|
|
305
|
+
configurePostgresApplicationName(opts.id);
|
|
306
|
+
const isDevelopment = env.IS_DEVELOPMENT;
|
|
284
307
|
const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
|
|
285
308
|
|
|
286
309
|
// ── 0. Register declared settings into the runtime registry ──────────
|
|
@@ -293,12 +316,14 @@ export const defineApp = <
|
|
|
293
316
|
}
|
|
294
317
|
|
|
295
318
|
// ── 1. SSR config ─────────────────────────────────────────────────────
|
|
296
|
-
const
|
|
319
|
+
const rootDir = opts.appRoot ?? process.cwd();
|
|
320
|
+
const { config, plugin, html } = createSsrConfig<ResolvedPageOptions>({
|
|
297
321
|
dev: isDevelopment,
|
|
298
322
|
verbose: true,
|
|
299
|
-
rootDir
|
|
323
|
+
rootDir,
|
|
324
|
+
componentRoots: [resolve(env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
|
|
300
325
|
basePath: opts.basePath,
|
|
301
|
-
template: ({ body, scripts, title, description, theme, lang }) => {
|
|
326
|
+
template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
|
|
302
327
|
const themeFixed = theme !== undefined;
|
|
303
328
|
return `<!DOCTYPE html>
|
|
304
329
|
<html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}>
|
|
@@ -321,6 +346,7 @@ export const defineApp = <
|
|
|
321
346
|
${body}
|
|
322
347
|
</body>
|
|
323
348
|
${scripts}
|
|
349
|
+
${performanceRoute ? `<script type="module" src="${WEB_VITALS_ASSET_HREF}" data-cloud-web-vitals data-app-id="${escapeHtml(opts.id)}" data-route-template="${escapeHtml(performanceRoute)}"></script>` : ""}
|
|
324
350
|
</html>`;
|
|
325
351
|
},
|
|
326
352
|
});
|
|
@@ -336,10 +362,16 @@ export const defineApp = <
|
|
|
336
362
|
// `getDateConfig` share one canonical `getLocale(c)` that page handlers
|
|
337
363
|
// cannot override.
|
|
338
364
|
const ssr = Object.assign(
|
|
339
|
-
createStatusPreservingSsrHandler<
|
|
365
|
+
createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
|
|
340
366
|
c.get("page").lang = getLocale(c);
|
|
341
367
|
const user = c.get("user");
|
|
342
|
-
|
|
368
|
+
c.get("page").performanceRoute =
|
|
369
|
+
user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
|
|
370
|
+
await Promise.all([
|
|
371
|
+
preloadLayoutAnnouncements(c),
|
|
372
|
+
preloadLayoutHelp(c),
|
|
373
|
+
user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
|
|
374
|
+
]);
|
|
343
375
|
}),
|
|
344
376
|
createPageResponses(html),
|
|
345
377
|
);
|
|
@@ -354,6 +386,7 @@ export const defineApp = <
|
|
|
354
386
|
adminHref: opts.adminHref,
|
|
355
387
|
adminNav: opts.adminNav?.map(
|
|
356
388
|
(group): AppAdminNavigationGroup => ({
|
|
389
|
+
...(group.section ? { section: group.section } : {}),
|
|
357
390
|
id: group.id,
|
|
358
391
|
label: group.label,
|
|
359
392
|
links: group.links.map((link) => ({ ...link })),
|
|
@@ -362,6 +395,7 @@ export const defineApp = <
|
|
|
362
395
|
routes: [...opts.routes],
|
|
363
396
|
nav: opts.nav,
|
|
364
397
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
398
|
+
searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
|
|
365
399
|
widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
|
|
366
400
|
settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
|
|
367
401
|
openapi: opts.openapi,
|
|
@@ -400,6 +434,8 @@ export const defineApp = <
|
|
|
400
434
|
}
|
|
401
435
|
})());
|
|
402
436
|
try {
|
|
437
|
+
bindProcessApplicationId(meta.id);
|
|
438
|
+
cleanup.unshift(clearProcessApplicationId);
|
|
403
439
|
const startedAt = Date.now();
|
|
404
440
|
|
|
405
441
|
// OpenAPI advertised in the registry only when there's a router to
|
|
@@ -410,8 +446,6 @@ export const defineApp = <
|
|
|
410
446
|
const compiledHelp = startOpts.help
|
|
411
447
|
? compileHelp({
|
|
412
448
|
appId: meta.id,
|
|
413
|
-
appName: meta.name,
|
|
414
|
-
appIcon: meta.icon,
|
|
415
449
|
basePath: opts.basePath,
|
|
416
450
|
definition: startOpts.help,
|
|
417
451
|
})
|
|
@@ -441,6 +475,7 @@ export const defineApp = <
|
|
|
441
475
|
}
|
|
442
476
|
: undefined,
|
|
443
477
|
adminNav: meta.adminNav?.map((group) => ({
|
|
478
|
+
...(group.section ? { section: group.section } : {}),
|
|
444
479
|
id: group.id,
|
|
445
480
|
label: group.label,
|
|
446
481
|
links: group.links.map((link) => ({ ...link })),
|
|
@@ -453,6 +488,7 @@ export const defineApp = <
|
|
|
453
488
|
: undefined,
|
|
454
489
|
help: compiledHelp?.summary,
|
|
455
490
|
legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
491
|
+
searchLinks: meta.searchLinks,
|
|
456
492
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
457
493
|
settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
|
|
458
494
|
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
@@ -461,6 +497,7 @@ export const defineApp = <
|
|
|
461
497
|
// Heartbeat
|
|
462
498
|
const heartbeat = createHeartbeat(meta.id, entry, {
|
|
463
499
|
registry: appRegistry(),
|
|
500
|
+
beforeWrite: compiledHelp ? () => registerHelp(compiledHelp.corpus) : undefined,
|
|
464
501
|
onError: (error) =>
|
|
465
502
|
log.error("Registry heartbeat failed", {
|
|
466
503
|
appId: meta.id,
|
|
@@ -499,27 +536,6 @@ export const defineApp = <
|
|
|
499
536
|
},
|
|
500
537
|
})
|
|
501
538
|
: undefined;
|
|
502
|
-
const helpHeartbeat = compiledHelp
|
|
503
|
-
? createHeartbeat(meta.id, compiledHelp.registryEntry, {
|
|
504
|
-
key: `help/${meta.id}`,
|
|
505
|
-
registry: helpRegistry(),
|
|
506
|
-
onError: (error) =>
|
|
507
|
-
log.error("Help registry heartbeat failed", {
|
|
508
|
-
appId: meta.id,
|
|
509
|
-
error: error instanceof Error ? error.message : String(error),
|
|
510
|
-
}),
|
|
511
|
-
onStale: (error) => {
|
|
512
|
-
log.error("Help registry lease expired; restarting", {
|
|
513
|
-
appId: meta.id,
|
|
514
|
-
error: error instanceof Error ? error.message : String(error),
|
|
515
|
-
});
|
|
516
|
-
process.exit(1);
|
|
517
|
-
},
|
|
518
|
-
})
|
|
519
|
-
: undefined;
|
|
520
|
-
cleanup.push(async () => {
|
|
521
|
-
await helpHeartbeat?.stop();
|
|
522
|
-
});
|
|
523
539
|
cleanup.push(async () => {
|
|
524
540
|
await capabilityHeartbeat?.stop();
|
|
525
541
|
});
|
|
@@ -614,6 +630,8 @@ export const defineApp = <
|
|
|
614
630
|
user,
|
|
615
631
|
idempotencyKey,
|
|
616
632
|
locale: requestLocale,
|
|
633
|
+
requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
|
|
634
|
+
origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
|
|
617
635
|
signal: c.req.raw.signal,
|
|
618
636
|
},
|
|
619
637
|
onUnexpectedError: (error: unknown) =>
|
|
@@ -654,6 +672,32 @@ export const defineApp = <
|
|
|
654
672
|
schemaHash: operation.manifest.schemaHash,
|
|
655
673
|
};
|
|
656
674
|
});
|
|
675
|
+
for (const kind of ["queries", "actions"] as const) {
|
|
676
|
+
server.post(
|
|
677
|
+
`/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
|
|
678
|
+
capabilityAuth(kind),
|
|
679
|
+
auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"),
|
|
680
|
+
async (c) => {
|
|
681
|
+
const actor = c.get("actor");
|
|
682
|
+
return invokeCapabilityStream({
|
|
683
|
+
compiled: compiledCapabilities,
|
|
684
|
+
kind,
|
|
685
|
+
localId: c.req.param("capabilityId") ?? "",
|
|
686
|
+
verb: c.req.param("verb") ?? "",
|
|
687
|
+
request: c.req.raw,
|
|
688
|
+
context: {
|
|
689
|
+
actor,
|
|
690
|
+
accessSubject: c.get("accessSubject"),
|
|
691
|
+
user: actor.kind === "user" ? actor.user : actor.delegatedUser,
|
|
692
|
+
locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
|
|
693
|
+
requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
|
|
694
|
+
origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
|
|
695
|
+
signal: c.req.raw.signal,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
}
|
|
657
701
|
const capabilityReadScope = auth.requireOAuthScope("read", "admin");
|
|
658
702
|
const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
|
|
659
703
|
server.post("/api/_internal/capabilities/v1/queries/:capabilityId", capabilityAuth("queries"), capabilityReadScope, (c) =>
|
|
@@ -759,7 +803,6 @@ export const defineApp = <
|
|
|
759
803
|
|
|
760
804
|
// Only advertise after setup, workers, and their declared resources are ready.
|
|
761
805
|
// Publish supporting entries first; the app entry makes them discoverable.
|
|
762
|
-
await helpHeartbeat?.start();
|
|
763
806
|
await capabilityHeartbeat?.start();
|
|
764
807
|
await heartbeat.start();
|
|
765
808
|
log.info(`Registered "${meta.id}"`, { baseUrl });
|
|
@@ -18,6 +18,8 @@ type HeartbeatRegistry<T> = {
|
|
|
18
18
|
type HeartbeatOptions<T> = {
|
|
19
19
|
registry: HeartbeatRegistry<T>;
|
|
20
20
|
key?: string;
|
|
21
|
+
/** Publish dependent data before advertising or renewing the app. */
|
|
22
|
+
beforeWrite?: () => Promise<void>;
|
|
21
23
|
intervalMs?: number;
|
|
22
24
|
retryMs?: number;
|
|
23
25
|
staleAfterMs?: number;
|
|
@@ -53,6 +55,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
|
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
const register = async (): Promise<void> => {
|
|
58
|
+
await options.beforeWrite?.();
|
|
56
59
|
await registry.upsert({ key, value: entry });
|
|
57
60
|
noteSuccess();
|
|
58
61
|
};
|
|
@@ -61,6 +64,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
|
|
|
61
64
|
// Touch renews the existing entry (and emits a registry invalidation).
|
|
62
65
|
// Avoid rebuilding the manifest here. A missing entry is repaired
|
|
63
66
|
// immediately, preserving restart-free registry recovery.
|
|
67
|
+
await options.beforeWrite?.();
|
|
64
68
|
const touched = await registry.touch({ key });
|
|
65
69
|
if (!touched) await registry.upsert({ key, value: entry });
|
|
66
70
|
noteSuccess();
|
|
@@ -1,49 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { helpLocaleChain } from "../shared/help";
|
|
1
|
+
import type { HelpArticle } from "../services/help/types";
|
|
3
2
|
import { markdownToPlainText } from "../shared/markdown";
|
|
4
|
-
import { getApp, getHelp, listApps, listHelp } from "./registry";
|
|
5
|
-
|
|
6
3
|
export const HELP_SEARCH_MAX_LIMIT = 25;
|
|
7
4
|
export const HELP_READ_MAX_CHARS = 7_000;
|
|
8
|
-
|
|
9
|
-
export type HelpCatalogDocument = {
|
|
10
|
-
appId: string;
|
|
11
|
-
appName: string;
|
|
12
|
-
appIcon?: string;
|
|
13
|
-
manifestHash: string;
|
|
14
|
-
locale: string;
|
|
15
|
-
documentId: string;
|
|
16
|
-
title: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
order: number;
|
|
19
|
-
markdown: string;
|
|
20
|
-
searchText: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type HelpCatalogItem = Omit<HelpCatalogDocument, "appIcon" | "manifestHash" | "order" | "markdown" | "searchText"> & {
|
|
24
|
-
kind: "help";
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type HelpCatalogRead = HelpCatalogItem & {
|
|
28
|
-
markdown: string;
|
|
29
|
-
truncated: boolean;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type HelpCatalogDependencies = {
|
|
33
|
-
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
34
|
-
listHelp?: () => Promise<HelpRegistryEntry[]>;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type AppHelpDependencies = {
|
|
38
|
-
getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
|
|
39
|
-
getHelp?: (appId: string) => Promise<HelpRegistryEntry | null>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type ResolvedAppHelp =
|
|
43
|
-
| { status: "available"; app: AppRegistryEntry; help: HelpRegistryEntry }
|
|
44
|
-
| { status: "missing" }
|
|
45
|
-
| { status: "stale" };
|
|
46
|
-
|
|
47
5
|
const normalizeSearchText = (value: string): string =>
|
|
48
6
|
value
|
|
49
7
|
.normalize("NFKD")
|
|
@@ -51,125 +9,7 @@ const normalizeSearchText = (value: string): string =>
|
|
|
51
9
|
.toLocaleLowerCase()
|
|
52
10
|
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
53
11
|
.trim();
|
|
54
|
-
|
|
55
|
-
const searchTerms = (value: string): string[] =>
|
|
56
|
-
Array.from(
|
|
57
|
-
new Set(
|
|
58
|
-
normalizeSearchText(value)
|
|
59
|
-
.split(" ")
|
|
60
|
-
.filter((term) => term.length > 0),
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
export const resolveAppHelp = async (appId: string, dependencies: AppHelpDependencies = {}): Promise<ResolvedAppHelp> => {
|
|
65
|
-
const [app, help] = await Promise.all([(dependencies.getApp ?? getApp)(appId), (dependencies.getHelp ?? getHelp)(appId)]);
|
|
66
|
-
if (!app?.help || !help) return { status: "missing" };
|
|
67
|
-
if (app.help.manifestHash !== help.manifestHash) return { status: "stale" };
|
|
68
|
-
return { status: "available", app, help };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const loadCurrentHelp = async (dependencies: HelpCatalogDependencies = {}): Promise<HelpRegistryEntry[]> => {
|
|
72
|
-
const [apps, entries] = await Promise.all([(dependencies.listApps ?? listApps)(), (dependencies.listHelp ?? listHelp)()]);
|
|
73
|
-
const manifestHashes = new Map(apps.flatMap((app) => (app.help ? [[app.id, app.help.manifestHash] as const] : [])));
|
|
74
|
-
return entries.filter((entry) => manifestHashes.get(entry.appId) === entry.manifestHash);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const resolveRegistryDocuments = (
|
|
78
|
-
entry: HelpRegistryEntry,
|
|
79
|
-
requestedLocale: string,
|
|
80
|
-
): { locale: string; documents: HelpRegistryDocument[] } => {
|
|
81
|
-
const baseLocale = entry.baseLocale ?? "en";
|
|
82
|
-
const variants = entry.documentsByLocale ?? {};
|
|
83
|
-
const chain = helpLocaleChain(requestedLocale, baseLocale);
|
|
84
|
-
const locale = chain.find((candidate) => candidate === baseLocale || variants[candidate] !== undefined) ?? baseLocale;
|
|
85
|
-
const byId = new Map(entry.documents.map((document) => [document.id, document]));
|
|
86
|
-
for (const candidate of [...chain.slice(0, chain.indexOf(baseLocale)), baseLocale].reverse()) {
|
|
87
|
-
if (candidate === baseLocale) continue;
|
|
88
|
-
for (const document of variants[candidate] ?? []) byId.set(document.id, { ...byId.get(document.id), ...document });
|
|
89
|
-
}
|
|
90
|
-
return { locale, documents: entry.documents.map((document) => byId.get(document.id) ?? document) };
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export const createHelpCatalog = (entries: readonly HelpRegistryEntry[], requestedLocale = "en"): HelpCatalogDocument[] =>
|
|
94
|
-
entries
|
|
95
|
-
.flatMap((entry) => {
|
|
96
|
-
const resolved = resolveRegistryDocuments(entry, requestedLocale);
|
|
97
|
-
return resolved.documents.map((document) => ({
|
|
98
|
-
appId: entry.appId,
|
|
99
|
-
appName: entry.appName,
|
|
100
|
-
appIcon: entry.appIcon,
|
|
101
|
-
manifestHash: entry.manifestHash,
|
|
102
|
-
locale: resolved.locale,
|
|
103
|
-
documentId: document.id,
|
|
104
|
-
title: document.title,
|
|
105
|
-
description: document.description,
|
|
106
|
-
order: document.order,
|
|
107
|
-
markdown: document.markdown,
|
|
108
|
-
searchText: document.searchText ?? markdownToPlainText(document.markdown),
|
|
109
|
-
}));
|
|
110
|
-
})
|
|
111
|
-
.sort(
|
|
112
|
-
(left, right) => left.appId.localeCompare(right.appId) || left.order - right.order || left.documentId.localeCompare(right.documentId),
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
export const loadHelpCatalog = async (dependencies: HelpCatalogDependencies = {}, requestedLocale = "en"): Promise<HelpCatalogDocument[]> =>
|
|
116
|
-
createHelpCatalog(await loadCurrentHelp(dependencies), requestedLocale);
|
|
117
|
-
|
|
118
|
-
const catalogItem = (document: HelpCatalogDocument): HelpCatalogItem => ({
|
|
119
|
-
appId: document.appId,
|
|
120
|
-
appName: document.appName,
|
|
121
|
-
kind: "help",
|
|
122
|
-
locale: document.locale,
|
|
123
|
-
documentId: document.documentId,
|
|
124
|
-
title: document.title,
|
|
125
|
-
description: document.description,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const searchScore = (document: HelpCatalogDocument, query: string): number => {
|
|
129
|
-
const phrase = normalizeSearchText(query);
|
|
130
|
-
const terms = searchTerms(query);
|
|
131
|
-
if (!phrase || terms.length === 0) return 0;
|
|
132
|
-
|
|
133
|
-
const app = normalizeSearchText(`${document.appId} ${document.appName}`);
|
|
134
|
-
const identity = normalizeSearchText(`${document.documentId} ${document.title}`);
|
|
135
|
-
const description = normalizeSearchText(document.description ?? "");
|
|
136
|
-
const body = normalizeSearchText(document.searchText);
|
|
137
|
-
let score = 0;
|
|
138
|
-
if (identity === phrase) score += 80;
|
|
139
|
-
else if (identity.includes(phrase)) score += 40;
|
|
140
|
-
if (app.includes(phrase)) score += 30;
|
|
141
|
-
if (description.includes(phrase)) score += 20;
|
|
142
|
-
if (body.includes(phrase)) score += 10;
|
|
143
|
-
for (const term of terms) {
|
|
144
|
-
if (identity.includes(term)) score += 8;
|
|
145
|
-
if (app.includes(term)) score += 6;
|
|
146
|
-
if (description.includes(term)) score += 4;
|
|
147
|
-
if (body.includes(term)) score += 1;
|
|
148
|
-
}
|
|
149
|
-
return score;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export const searchHelpCatalog = (
|
|
153
|
-
catalog: readonly HelpCatalogDocument[],
|
|
154
|
-
input: { query: string; appId?: string; limit?: number },
|
|
155
|
-
): HelpCatalogItem[] => {
|
|
156
|
-
const limit = Math.max(1, Math.min(Math.floor(input.limit ?? 10), HELP_SEARCH_MAX_LIMIT));
|
|
157
|
-
return catalog
|
|
158
|
-
.filter((document) => !input.appId || document.appId === input.appId)
|
|
159
|
-
.map((document) => ({ document, score: searchScore(document, input.query) }))
|
|
160
|
-
.filter(({ score }) => score > 0)
|
|
161
|
-
.sort(
|
|
162
|
-
(left, right) =>
|
|
163
|
-
right.score - left.score ||
|
|
164
|
-
left.document.appId.localeCompare(right.document.appId) ||
|
|
165
|
-
left.document.documentId.localeCompare(right.document.documentId),
|
|
166
|
-
)
|
|
167
|
-
.slice(0, limit)
|
|
168
|
-
.map(({ document }) => catalogItem(document));
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
export const findHelpDocument = (catalog: readonly HelpCatalogDocument[], appId: string, documentId: string): HelpCatalogDocument | null =>
|
|
172
|
-
catalog.find((document) => document.appId === appId && document.documentId === documentId) ?? null;
|
|
12
|
+
const searchTerms = (value: string): string[] => [...new Set(normalizeSearchText(value).split(" ").filter(Boolean))];
|
|
173
13
|
|
|
174
14
|
const splitSections = (markdown: string): string[] => {
|
|
175
15
|
const starts = [
|
|
@@ -193,7 +33,21 @@ const boundedExcerpt = (markdown: string, terms: readonly string[]): string => {
|
|
|
193
33
|
return `${start > 0 ? `${marker}\n\n` : ""}${body}${start + budget < markdown.length ? `\n\n${marker}` : ""}`;
|
|
194
34
|
};
|
|
195
35
|
|
|
196
|
-
const
|
|
36
|
+
export const selectHelpMarkdown = (markdown: string, query?: string): { markdown: string; truncated: boolean } => {
|
|
37
|
+
// An exact heading identifies one complete reference section, even in short articles.
|
|
38
|
+
const headingQuery = query?.trim().toLocaleLowerCase();
|
|
39
|
+
const exact =
|
|
40
|
+
headingQuery &&
|
|
41
|
+
splitSections(markdown).find((section) => {
|
|
42
|
+
const heading = section
|
|
43
|
+
.match(/^##\s+([^\n]+)/)?.[1]
|
|
44
|
+
?.replace(/\s*\{icon="[^"]*"\}\s*$/, "")
|
|
45
|
+
.replaceAll("`", "")
|
|
46
|
+
.trim()
|
|
47
|
+
.toLocaleLowerCase();
|
|
48
|
+
return heading === headingQuery;
|
|
49
|
+
});
|
|
50
|
+
if (exact) return { markdown: boundedExcerpt(exact, [headingQuery]), truncated: exact !== markdown };
|
|
197
51
|
if (markdown.length <= HELP_READ_MAX_CHARS) return { markdown, truncated: false };
|
|
198
52
|
const terms = searchTerms(query ?? "");
|
|
199
53
|
const phrase = normalizeSearchText(query ?? "");
|
|
@@ -217,15 +71,16 @@ const selectMarkdown = (markdown: string, query?: string): { markdown: string; t
|
|
|
217
71
|
return { markdown: excerpt || boundedExcerpt(markdown, terms), truncated: true };
|
|
218
72
|
};
|
|
219
73
|
|
|
220
|
-
export const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
74
|
+
export const readHelpArticle = (document: HelpArticle, query?: string) => ({
|
|
75
|
+
kind: "help" as const,
|
|
76
|
+
appId: document.appId,
|
|
77
|
+
appName: document.appName,
|
|
78
|
+
locale: document.locale,
|
|
79
|
+
documentId: document.documentId,
|
|
80
|
+
title: document.title,
|
|
81
|
+
description: document.description,
|
|
82
|
+
...selectHelpMarkdown(document.markdown, query),
|
|
83
|
+
});
|
|
229
84
|
|
|
230
85
|
export const helpResourceUri = (appId: string, documentId: string): string =>
|
|
231
86
|
`cloud://help/${encodeURIComponent(appId)}/${encodeURIComponent(documentId)}`;
|
package/src/_internal/help.ts
CHANGED
|
@@ -1,42 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import type { AppRegistryHelpSummary
|
|
2
|
+
import type { AppRegistryHelpSummary } from "../contracts/registry";
|
|
3
3
|
import type { HelpDefinition } from "../server/help";
|
|
4
|
-
import type {
|
|
4
|
+
import type { HelpCorpus, HelpDocument } from "../services/help/types";
|
|
5
5
|
|
|
6
|
-
export const HELP_REGISTRY_MAX_BYTES = 512 * 1024;
|
|
7
6
|
export const HELP_DOCUMENT_MAX_BYTES = 128 * 1024;
|
|
7
|
+
export type CompiledHelp = { summary: AppRegistryHelpSummary; corpus: HelpCorpus };
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
registryEntry: HelpRegistryEntry;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const serializedBytes = (value: unknown): number => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
15
|
-
|
|
16
|
-
const helpHash = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
17
|
-
|
|
18
|
-
const omitSearchText = (documents: readonly HelpRegistryEntry["documents"][number][]) =>
|
|
19
|
-
documents.map(({ searchText: _searchText, ...document }) => document);
|
|
20
|
-
|
|
21
|
-
const normalizedBasePath = (basePath: string | undefined): string => {
|
|
22
|
-
const value = basePath?.replace(/\/$/, "") ?? "";
|
|
23
|
-
return value || "";
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const compileHelp = (input: {
|
|
27
|
-
appId: string;
|
|
28
|
-
appName: string;
|
|
29
|
-
appIcon: string;
|
|
30
|
-
basePath?: string;
|
|
31
|
-
definition: HelpDefinition;
|
|
32
|
-
}): CompiledHelp => {
|
|
33
|
-
const appId = encodeURIComponent(input.appId);
|
|
34
|
-
const pageBase = `${normalizedBasePath(input.basePath)}/help`;
|
|
35
|
-
const searchUrl = `/api/help/v1/${appId}/search`;
|
|
36
|
-
const compileDocuments = (source: readonly (typeof input.definition.documents)[number][]) =>
|
|
9
|
+
export const compileHelp = (input: { appId: string; basePath?: string; definition: HelpDefinition }): CompiledHelp => {
|
|
10
|
+
const compileDocuments = (source: HelpDefinition["documents"]): HelpDocument[] =>
|
|
37
11
|
source.map(({ id, title, icon, description, order, markdown, searchText }) => {
|
|
38
|
-
|
|
39
|
-
if (markdownBytes > HELP_DOCUMENT_MAX_BYTES) {
|
|
12
|
+
if (Buffer.byteLength(markdown) > HELP_DOCUMENT_MAX_BYTES) {
|
|
40
13
|
throw new Error(`Help document "${id}" exceeds the ${HELP_DOCUMENT_MAX_BYTES}-byte limit`);
|
|
41
14
|
}
|
|
42
15
|
return { id, title, icon, description, order, markdown, searchText };
|
|
@@ -46,50 +19,14 @@ export const compileHelp = (input: {
|
|
|
46
19
|
const documentsByLocale = Object.fromEntries(
|
|
47
20
|
Object.entries(input.definition.documentsByLocale ?? {})
|
|
48
21
|
.filter(([locale]) => locale !== baseLocale)
|
|
49
|
-
.
|
|
22
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
23
|
+
.map(([locale, docs]) => [locale, compileDocuments(docs)]),
|
|
50
24
|
);
|
|
51
|
-
const manifestHash =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
id,
|
|
55
|
-
title,
|
|
56
|
-
icon,
|
|
57
|
-
description,
|
|
58
|
-
order,
|
|
59
|
-
searchUrl,
|
|
60
|
-
url: `/api/help/v1/${appId}/documents/${encodeURIComponent(id)}`,
|
|
61
|
-
}));
|
|
62
|
-
const manifest = toManifest(documents);
|
|
63
|
-
const manifestByLocale = Object.fromEntries(
|
|
64
|
-
Object.entries(documentsByLocale).map(([locale, localized]) => [locale, toManifest(localized)]),
|
|
65
|
-
);
|
|
66
|
-
let registryEntry: HelpRegistryEntry = {
|
|
67
|
-
appId: input.appId,
|
|
68
|
-
appName: input.appName,
|
|
69
|
-
appIcon: input.appIcon,
|
|
70
|
-
manifestHash,
|
|
71
|
-
baseLocale,
|
|
72
|
-
documentsByLocale,
|
|
73
|
-
documents,
|
|
74
|
-
};
|
|
75
|
-
let bytes = serializedBytes(registryEntry);
|
|
76
|
-
if (bytes > HELP_REGISTRY_MAX_BYTES) {
|
|
77
|
-
registryEntry = {
|
|
78
|
-
...registryEntry,
|
|
79
|
-
documents: omitSearchText(registryEntry.documents),
|
|
80
|
-
documentsByLocale: registryEntry.documentsByLocale
|
|
81
|
-
? Object.fromEntries(
|
|
82
|
-
Object.entries(registryEntry.documentsByLocale).map(([locale, localized]) => [locale, omitSearchText(localized)]),
|
|
83
|
-
)
|
|
84
|
-
: undefined,
|
|
85
|
-
};
|
|
86
|
-
bytes = serializedBytes(registryEntry);
|
|
87
|
-
}
|
|
88
|
-
if (bytes > HELP_REGISTRY_MAX_BYTES) {
|
|
89
|
-
throw new Error(`Help corpus exceeds the ${HELP_REGISTRY_MAX_BYTES}-byte registry limit`);
|
|
90
|
-
}
|
|
25
|
+
const manifestHash = createHash("sha256")
|
|
26
|
+
.update(JSON.stringify({ revision: 1, appId: input.appId, baseLocale, documents, documentsByLocale }))
|
|
27
|
+
.digest("hex");
|
|
91
28
|
return {
|
|
92
|
-
summary: { manifestHash, pageBase
|
|
93
|
-
|
|
29
|
+
summary: { manifestHash, pageBase: `${input.basePath?.replace(/\/$/, "") ?? ""}/help`, baseLocale },
|
|
30
|
+
corpus: { appId: input.appId, manifestHash, baseLocale, documents, documentsByLocale },
|
|
94
31
|
};
|
|
95
32
|
};
|
|
@@ -2,6 +2,7 @@ import type { HtmlFn } from "@k2b/ssr";
|
|
|
2
2
|
import type { Context } from "hono";
|
|
3
3
|
import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status";
|
|
4
4
|
import { getLocale } from "../server/locale";
|
|
5
|
+
import { preloadLayoutAnnouncements } from "../server/middleware/settings";
|
|
5
6
|
import { createLoginRedirectUrl } from "../shared/redirect";
|
|
6
7
|
import type { PageOptions } from "./define-app";
|
|
7
8
|
|
|
@@ -21,6 +22,7 @@ export const createPageResponses = (html: HtmlFn<PageOptions>) => {
|
|
|
21
22
|
// Auth middleware may reject before ssr() initializes page metadata.
|
|
22
23
|
c.set("page", { lang: getLocale(c) });
|
|
23
24
|
c.header("Cache-Control", "private, no-store");
|
|
25
|
+
await preloadLayoutAnnouncements(c);
|
|
24
26
|
// Load JSX only after the application's SSR plugin has been installed.
|
|
25
27
|
const { renderPageError } = await import("../ssr/PageError");
|
|
26
28
|
const response = await html(renderPageError(c, status, options), c.get("page"));
|