@k2b/cloud 0.6.0-rc.0 → 0.7.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 +30 -17
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- 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/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 +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -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 +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- 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 +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -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 +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -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 +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -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 +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -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-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- 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 +66 -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 +5 -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 +114 -34
- 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 +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/solid.tsx +3 -3
- package/src/index.ts +0 -4
- 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/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -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/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- 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 +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- 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 +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -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 +107 -84
- package/src/services/pdf/index.ts +3 -0
- 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 +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- 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 +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- 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 +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- 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 +7 -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/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- 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 +2 -0
- 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/scripts/app-favicon.test.ts +0 -49
- package/src/ai/chat/blocks.render.test.tsx +0 -1108
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
- package/src/ssr/Layout.render.test.tsx +0 -148
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -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
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { preloadLayoutAnnouncements } from "../server/middleware/settings";
|
|
4
5
|
import { getLocale } from "../server/locale";
|
|
5
6
|
import { createLoginRedirectUrl } from "../shared/redirect";
|
|
6
7
|
import type { PageOptions } from "./define-app";
|
|
@@ -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"));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Name Bun's default pool before its first use without changing connection ownership. */
|
|
2
|
+
export const configurePostgresApplicationName = (
|
|
3
|
+
applicationId: string,
|
|
4
|
+
environment: Record<string, string | undefined> = process.env,
|
|
5
|
+
): void => {
|
|
6
|
+
// Match Bun's precedence and preserve TLS_* variables, which imply required TLS.
|
|
7
|
+
const variable = ["DATABASE_URL", "DATABASEURL", "TLS_DATABASE_URL", "POSTGRES_URL", "PGURL", "PG_URL", "TLS_POSTGRES_DATABASE_URL"].find(
|
|
8
|
+
(key) => environment[key],
|
|
9
|
+
);
|
|
10
|
+
if (!variable) return;
|
|
11
|
+
|
|
12
|
+
let url: URL;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(environment[variable]!);
|
|
15
|
+
} catch {
|
|
16
|
+
// Diagnostics must not change how Bun handles an existing connection configuration.
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return;
|
|
20
|
+
|
|
21
|
+
let operatorConfigured = false;
|
|
22
|
+
url.searchParams.forEach((_value, key) => {
|
|
23
|
+
// options can contain an operator-owned `-c application_name=...` setting.
|
|
24
|
+
if (key.toLowerCase() === "application_name" || key.toLowerCase() === "options") operatorConfigured = true;
|
|
25
|
+
});
|
|
26
|
+
if (operatorConfigured) return;
|
|
27
|
+
|
|
28
|
+
url.searchParams.set("application_name", `cloud:${applicationId}`);
|
|
29
|
+
environment[variable] = url.toString();
|
|
30
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Identity of the application started in this process, derived from its declaration. */
|
|
2
|
+
let applicationId: string | undefined;
|
|
3
|
+
|
|
4
|
+
export const getProcessApplicationId = (): string | undefined => applicationId;
|
|
5
|
+
|
|
6
|
+
export const bindProcessApplicationId = (id: string): void => {
|
|
7
|
+
if (applicationId !== undefined && applicationId !== id) {
|
|
8
|
+
throw new Error(`This process already hosts application "${applicationId}"`);
|
|
9
|
+
}
|
|
10
|
+
applicationId = id;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const clearProcessApplicationId = (): void => {
|
|
14
|
+
applicationId = undefined;
|
|
15
|
+
};
|
|
@@ -82,7 +82,7 @@ export const startProcessSync = async ({ application }: { application: string })
|
|
|
82
82
|
connection,
|
|
83
83
|
namespace: env.SYNC_NAMESPACE,
|
|
84
84
|
application,
|
|
85
|
-
defaults: { replicas:
|
|
85
|
+
defaults: { replicas: env.SYNC_REPLICAS },
|
|
86
86
|
observe: (event) => observeSyncEvent(event, application),
|
|
87
87
|
});
|
|
88
88
|
bindProcessSync(sync);
|
|
@@ -39,11 +39,12 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
39
39
|
}
|
|
40
40
|
for (const [locale, translation] of Object.entries(value.presentation.translations)) {
|
|
41
41
|
if (!isRecord(translation)) return invalid(`presentation.translations.${locale}`, "an object");
|
|
42
|
-
if (translation.name !== undefined && !isString(translation.name))
|
|
42
|
+
if (translation.name !== undefined && !isString(translation.name))
|
|
43
|
+
return invalid(`presentation.translations.${locale}.name`, "a string");
|
|
43
44
|
if (translation.description !== undefined && !isString(translation.description)) {
|
|
44
45
|
return invalid(`presentation.translations.${locale}.description`, "a string");
|
|
45
46
|
}
|
|
46
|
-
for (const field of ["adminGroups", "adminLinks", "legalLinks"] as const) {
|
|
47
|
+
for (const field of ["adminGroups", "adminLinks", "legalLinks", "searchLinks", "searchLinkDescriptions"] as const) {
|
|
47
48
|
if (translation[field] !== undefined && !isStringRecord(translation[field])) {
|
|
48
49
|
return invalid(`presentation.translations.${locale}.${field}`, "a string map");
|
|
49
50
|
}
|
|
@@ -66,6 +67,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
66
67
|
(group) =>
|
|
67
68
|
!isRecord(group) ||
|
|
68
69
|
(group.id !== undefined && !isString(group.id)) ||
|
|
70
|
+
(group.section !== undefined && group.section !== "ai") ||
|
|
69
71
|
!isString(group.label) ||
|
|
70
72
|
!Array.isArray(group.links) ||
|
|
71
73
|
group.links.some((link) => !isRecord(link) || !isString(link.label) || !isString(link.href) || !isString(link.icon)),
|
|
@@ -84,6 +86,23 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
84
86
|
return invalid("legalLinks", "an array of valid links");
|
|
85
87
|
}
|
|
86
88
|
}
|
|
89
|
+
if (value.searchLinks !== undefined) {
|
|
90
|
+
if (
|
|
91
|
+
!Array.isArray(value.searchLinks) ||
|
|
92
|
+
value.searchLinks.some(
|
|
93
|
+
(link) =>
|
|
94
|
+
!isRecord(link) ||
|
|
95
|
+
!isString(link.label) ||
|
|
96
|
+
!link.label.trim() ||
|
|
97
|
+
(link.description !== undefined && !isString(link.description)) ||
|
|
98
|
+
!isString(link.href) ||
|
|
99
|
+
!/^\/(?![\/\\])[^\\\s]*$/.test(link.href) ||
|
|
100
|
+
(link.icon !== undefined && !isString(link.icon)) ||
|
|
101
|
+
(link.keywords !== undefined && !isStringArray(link.keywords)),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return invalid("searchLinks", "an array of same-origin navigation links");
|
|
105
|
+
}
|
|
87
106
|
if (value.widgets !== undefined) {
|
|
88
107
|
if (
|
|
89
108
|
!Array.isArray(value.widgets) ||
|
|
@@ -102,32 +121,17 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
102
121
|
return invalid("capabilities", "a valid capability summary");
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
if (
|
|
125
|
+
value.help !== undefined &&
|
|
126
|
+
(!isRecord(value.help) ||
|
|
108
127
|
!isString(value.help.manifestHash) ||
|
|
128
|
+
!isString(value.help.baseLocale) ||
|
|
109
129
|
!isString(value.help.pageBase) ||
|
|
110
|
-
!
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (
|
|
115
|
-
!value.help.pageBase.startsWith("/") ||
|
|
116
|
-
value.help.documents.some(
|
|
117
|
-
(document) =>
|
|
118
|
-
!isRecord(document) ||
|
|
119
|
-
!isString(document.id) ||
|
|
120
|
-
!isString(document.title) ||
|
|
121
|
-
typeof document.order !== "number" ||
|
|
122
|
-
!isString(document.searchUrl) ||
|
|
123
|
-
!document.searchUrl.startsWith("/") ||
|
|
124
|
-
!isString(document.url) ||
|
|
125
|
-
!document.url.startsWith("/"),
|
|
126
|
-
)
|
|
127
|
-
) {
|
|
128
|
-
return invalid("help", "a valid Help summary");
|
|
129
|
-
}
|
|
130
|
+
!value.help.pageBase.startsWith("/"))
|
|
131
|
+
) {
|
|
132
|
+
return invalid("help", "a valid Help reference");
|
|
130
133
|
}
|
|
134
|
+
|
|
131
135
|
if (value.openapi !== undefined && !isString(value.openapi)) return invalid("openapi", "a string");
|
|
132
136
|
return null;
|
|
133
137
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EphemeralConfig } from "@k2b/sync";
|
|
2
2
|
import type { AppAppearanceColor } from "../contracts/app";
|
|
3
3
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
|
|
4
|
-
import type { AppRegistryEntry, CapabilityRegistryEntry
|
|
4
|
+
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
5
5
|
import type { DashboardWidgetPresentation } from "../contracts/widgets";
|
|
6
6
|
import { resolveAppPresentations } from "../shared/app-presentation";
|
|
7
7
|
import { compileCapabilityPresentation, parseCapabilityManifest } from "./capabilities";
|
|
@@ -10,7 +10,7 @@ import { validateAppRegistryEntry } from "./registry-validation";
|
|
|
10
10
|
import { watchRegistryChanges } from "./registry-watch";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Shared app registry:
|
|
13
|
+
* Shared app registry: two @k2b/sync ephemerals on the process Sync
|
|
14
14
|
* instance. Apps, the gateway, and gateway-ops all declare them through the
|
|
15
15
|
* functions below, so id, owner, TTL, and value bound stay identical across
|
|
16
16
|
* the fleet (a differing declaration fails with ResourceDriftError).
|
|
@@ -34,20 +34,12 @@ export const CAPABILITY_REGISTRY_CONFIG = {
|
|
|
34
34
|
maxValueBytes: 512 * 1024,
|
|
35
35
|
} as const satisfies EphemeralConfig;
|
|
36
36
|
|
|
37
|
-
export const HELP_REGISTRY_CONFIG = {
|
|
38
|
-
id: "cloud-help",
|
|
39
|
-
owner: REGISTRY_OWNER,
|
|
40
|
-
ttlMs: APP_REGISTRY_TTL_MS,
|
|
41
|
-
maxValueBytes: 512 * 1024,
|
|
42
|
-
} as const satisfies EphemeralConfig;
|
|
43
|
-
|
|
44
37
|
export const APP_REGISTRY_PREFIX = "apps/";
|
|
45
38
|
|
|
46
39
|
export type CapabilityRegistryRecord = { appId: string; manifest: CapabilityManifest; presentation?: CapabilityPresentationCatalog };
|
|
47
40
|
|
|
48
41
|
export const appRegistry = lazySync((sync) => sync.ephemeral<AppRegistryEntry>(APP_REGISTRY_CONFIG));
|
|
49
42
|
export const capabilityRegistry = lazySync((sync) => sync.ephemeral<CapabilityRegistryRecord>(CAPABILITY_REGISTRY_CONFIG));
|
|
50
|
-
export const helpRegistry = lazySync((sync) => sync.ephemeral<HelpRegistryEntry>(HELP_REGISTRY_CONFIG));
|
|
51
43
|
|
|
52
44
|
/**
|
|
53
45
|
* Follow the app registry until `signal` aborts. The watch first replays the
|
|
@@ -232,49 +224,6 @@ export const getCapability = async (appId: string): Promise<CapabilityRegistryEn
|
|
|
232
224
|
return entry ? resolveLiveCapabilityRegistryEntry(entry.key, entry.value, app ?? undefined) : null;
|
|
233
225
|
};
|
|
234
226
|
|
|
235
|
-
const isHelpRegistryEntry = (value: unknown): value is HelpRegistryEntry => {
|
|
236
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
237
|
-
const entry = value as Partial<HelpRegistryEntry>;
|
|
238
|
-
const validDocument = (document: unknown) =>
|
|
239
|
-
!!document &&
|
|
240
|
-
typeof document === "object" &&
|
|
241
|
-
typeof (document as { id?: unknown }).id === "string" &&
|
|
242
|
-
typeof (document as { title?: unknown }).title === "string" &&
|
|
243
|
-
typeof (document as { order?: unknown }).order === "number" &&
|
|
244
|
-
typeof (document as { markdown?: unknown }).markdown === "string" &&
|
|
245
|
-
((document as { searchText?: unknown }).searchText === undefined ||
|
|
246
|
-
typeof (document as { searchText?: unknown }).searchText === "string") &&
|
|
247
|
-
((document as { icon?: unknown }).icon === undefined || typeof (document as { icon?: unknown }).icon === "string") &&
|
|
248
|
-
((document as { description?: unknown }).description === undefined ||
|
|
249
|
-
typeof (document as { description?: unknown }).description === "string");
|
|
250
|
-
return (
|
|
251
|
-
typeof entry.appId === "string" &&
|
|
252
|
-
typeof entry.appName === "string" &&
|
|
253
|
-
typeof entry.appIcon === "string" &&
|
|
254
|
-
typeof entry.manifestHash === "string" &&
|
|
255
|
-
(entry.baseLocale === undefined || typeof entry.baseLocale === "string") &&
|
|
256
|
-
Array.isArray(entry.documents) &&
|
|
257
|
-
entry.documents.every(validDocument) &&
|
|
258
|
-
(entry.documentsByLocale === undefined ||
|
|
259
|
-
(!!entry.documentsByLocale &&
|
|
260
|
-
typeof entry.documentsByLocale === "object" &&
|
|
261
|
-
!Array.isArray(entry.documentsByLocale) &&
|
|
262
|
-
Object.values(entry.documentsByLocale).every((documents) => Array.isArray(documents) && documents.every(validDocument))))
|
|
263
|
-
);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
export const listHelp = async (): Promise<HelpRegistryEntry[]> => {
|
|
267
|
-
const snap = await helpRegistry().snapshot({ prefix: "help/" });
|
|
268
|
-
return snap.entries.map((entry) => entry.value).filter(isHelpRegistryEntry);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export const getHelp = async (appId: string): Promise<HelpRegistryEntry | null> => {
|
|
272
|
-
const key = `help/${appId}`;
|
|
273
|
-
const snap = await helpRegistry().snapshot({ prefix: key });
|
|
274
|
-
const value = snap.entries.find((entry) => entry.key === key)?.value;
|
|
275
|
-
return isHelpRegistryEntry(value) ? value : null;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
227
|
/**
|
|
279
228
|
* Same as `listApps` but returns registry metadata for admin observability.
|
|
280
229
|
*/
|
|
@@ -29,6 +29,8 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
29
29
|
adminGroups: translation.adminGroups ? { ...translation.adminGroups } : undefined,
|
|
30
30
|
adminLinks: translation.adminLinks ? { ...translation.adminLinks } : undefined,
|
|
31
31
|
legalLinks: translation.legalLinks ? { ...translation.legalLinks } : undefined,
|
|
32
|
+
searchLinks: translation.searchLinks ? { ...translation.searchLinks } : undefined,
|
|
33
|
+
searchLinkDescriptions: translation.searchLinkDescriptions ? { ...translation.searchLinkDescriptions } : undefined,
|
|
32
34
|
},
|
|
33
35
|
]),
|
|
34
36
|
),
|
|
@@ -37,6 +39,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
37
39
|
appearance: e.appearance,
|
|
38
40
|
adminHref: e.nav?.adminHref,
|
|
39
41
|
adminNav: e.adminNav?.map((group) => ({
|
|
42
|
+
...(group.section ? {section:group.section} : {}),
|
|
40
43
|
id: group.id,
|
|
41
44
|
label: group.label,
|
|
42
45
|
links: group.links.map((link) => ({ ...link })),
|
|
@@ -53,22 +56,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
53
56
|
requiresRoles: e.nav.requiresRoles as Role[] | undefined,
|
|
54
57
|
}
|
|
55
58
|
: undefined,
|
|
56
|
-
help: e.help
|
|
57
|
-
? {
|
|
58
|
-
manifestHash: e.help.manifestHash,
|
|
59
|
-
pageBase: e.help.pageBase,
|
|
60
|
-
baseLocale: e.help.baseLocale,
|
|
61
|
-
documentsByLocale: e.help.documentsByLocale
|
|
62
|
-
? Object.fromEntries(
|
|
63
|
-
Object.entries(e.help.documentsByLocale).map(([locale, documents]) => [
|
|
64
|
-
locale,
|
|
65
|
-
documents.map((document) => ({ ...document })),
|
|
66
|
-
]),
|
|
67
|
-
)
|
|
68
|
-
: undefined,
|
|
69
|
-
documents: e.help.documents.map((document) => ({ ...document })),
|
|
70
|
-
}
|
|
71
|
-
: undefined,
|
|
59
|
+
help: e.help ? { ...e.help } : undefined,
|
|
72
60
|
searchTags,
|
|
73
61
|
searchHelp: searchQueries?.map((query) => query.description).join(" "),
|
|
74
62
|
searchTagHelp: searchQueries?.flatMap((query) =>
|
|
@@ -78,6 +66,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
78
66
|
]),
|
|
79
67
|
),
|
|
80
68
|
legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
69
|
+
searchLinks: e.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
|
|
81
70
|
openapi: e.openapi,
|
|
82
71
|
};
|
|
83
72
|
}),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
|
|
3
|
+
type Phase = "auth" | "settings" | "runtime" | "ssr_data" | "ssr_finalize" | "ssr_render";
|
|
4
|
+
|
|
5
|
+
/** Measure the owning operation, not downstream middleware or another request. */
|
|
6
|
+
export async function measureServerPhase<T>(c: Context, phase: Phase, operation: () => T | Promise<T>): Promise<T> {
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
try {
|
|
9
|
+
return await operation();
|
|
10
|
+
} finally {
|
|
11
|
+
c.header("Server-Timing", `${phase};dur=${(performance.now() - start).toFixed(2)}`, { append: true });
|
|
12
|
+
}
|
|
13
|
+
}
|