@k2b/cloud 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type AppSkillDefinition, appSkillManifestHash, registerAppSkills, validateAppSkillCatalog } from "../ai/app-skills";
|
|
2
|
+
import { type AppAccessDeclaration, compileAppAccess } from "../contracts/app-access";
|
|
2
3
|
import { startLiveOutbox } from "../events/live";
|
|
3
4
|
import { registerHelp } from "../services/help";
|
|
4
5
|
import { preloadLayoutHelp } from "../ssr/help";
|
|
6
|
+
import { registerPageSwap } from "../ssr/page-swap";
|
|
5
7
|
import { preloadPanelConsent } from "../ssr/panel-consent";
|
|
6
8
|
import { invokeCapabilityStream } from "./capability-streams";
|
|
9
|
+
import { withCapabilityRequestTimeout } from "./capability-timeout";
|
|
7
10
|
import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
|
|
8
11
|
/**
|
|
9
12
|
* defineApp() — The single entry point for every cloud app.
|
|
@@ -49,8 +52,10 @@ import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
|
49
52
|
import type { Role } from "../contracts/shared";
|
|
50
53
|
import type { HelpDefinition } from "../server/help";
|
|
51
54
|
import { getLocale, resolveLocale } from "../server/locale";
|
|
55
|
+
import { requireAppAccess, stopAppAccessConnections } from "../server/middleware/app-access";
|
|
52
56
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
53
57
|
import { requireInvocation } from "../server/middleware/invocation";
|
|
58
|
+
import "../events/app-access";
|
|
54
59
|
import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
|
|
55
60
|
import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
|
|
56
61
|
import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
|
|
@@ -86,6 +91,7 @@ import { cliPluginRoutePrefixes, createCliPluginRoutes, validateAppCliModules }
|
|
|
86
91
|
import { FONT_PRELOAD_LINKS } from "./font-preloads";
|
|
87
92
|
import { createHeartbeat, createRegistryMembership } from "./heartbeat";
|
|
88
93
|
import { compileHelp } from "./help";
|
|
94
|
+
import { createHttpDrain } from "./http-drain";
|
|
89
95
|
import { createPageResponses } from "./page-responses";
|
|
90
96
|
import { configurePostgresApplicationName } from "./postgres-application-name";
|
|
91
97
|
import { getProcessSync, startProcessSync } from "./process-sync";
|
|
@@ -129,6 +135,7 @@ type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePrefe
|
|
|
129
135
|
*/
|
|
130
136
|
export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefinitionMap = {}, AppId extends string = string> = {
|
|
131
137
|
id: AppId;
|
|
138
|
+
access?: AppAccessDeclaration;
|
|
132
139
|
name: string;
|
|
133
140
|
icon: string;
|
|
134
141
|
description: string;
|
|
@@ -137,6 +144,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
137
144
|
appearance?: AppAppearance;
|
|
138
145
|
/** URL prefix for SSR asset isolation. Omit for the global `/_ssr/` path (core). */
|
|
139
146
|
basePath?: string;
|
|
147
|
+
/** Swap Layout content within this app while keeping the rail, tab bar and panel document. Default: false; focusMode pages still load a document. */
|
|
148
|
+
pageSwap?: boolean;
|
|
140
149
|
/** Base URL as seen by other containers (e.g. "http://app-notebooks:3000"). */
|
|
141
150
|
baseUrl: string;
|
|
142
151
|
adminHref?: string;
|
|
@@ -530,6 +539,8 @@ export const defineApp = <
|
|
|
530
539
|
// cannot override.
|
|
531
540
|
const ssr = Object.assign(
|
|
532
541
|
createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
|
|
542
|
+
if (opts.pageSwap)
|
|
543
|
+
registerPageSwap(c, isDevelopment ? String(developmentStamp) : `${buildMetadata.version}:${buildMetadata.release}`);
|
|
533
544
|
c.get("page").lang = getLocale(c);
|
|
534
545
|
c.get("page").themePreference = readThemePreferenceFromCookieHeader(c.req.header("Cookie"));
|
|
535
546
|
const user = c.get("user");
|
|
@@ -578,13 +589,19 @@ export const defineApp = <
|
|
|
578
589
|
openapi: opts.openapi,
|
|
579
590
|
cli: opts.cli,
|
|
580
591
|
};
|
|
581
|
-
const meta: AppMeta = {
|
|
592
|
+
const meta: AppMeta = {
|
|
593
|
+
...baseMeta,
|
|
594
|
+
access: compileAppAccess(opts.access),
|
|
595
|
+
presentation: compileAppPresentation(baseMeta, opts.presentation),
|
|
596
|
+
};
|
|
582
597
|
|
|
583
598
|
// ── 3. start() — builds and boots the Hono server ────────────────────
|
|
584
599
|
const start = async (startOpts: StartOptions): Promise<StartResult> => {
|
|
585
600
|
const port = startOpts.port ?? 3000;
|
|
586
601
|
const baseUrl = opts.baseUrl;
|
|
587
602
|
const log = logger("app");
|
|
603
|
+
const http = createHttpDrain();
|
|
604
|
+
if (!meta.access) log.warn("Application has no access declaration; legacy entry and discovery rules remain active", { appId: meta.id });
|
|
588
605
|
|
|
589
606
|
// Settings are encrypted at rest, so every container needs the secret. It
|
|
590
607
|
// is only read on the first decrypt, which meant a container without it
|
|
@@ -632,6 +649,7 @@ export const defineApp = <
|
|
|
632
649
|
// Registry entry
|
|
633
650
|
const entry: AppRegistryEntry = {
|
|
634
651
|
id: meta.id,
|
|
652
|
+
access: meta.access,
|
|
635
653
|
name: meta.name,
|
|
636
654
|
icon: meta.icon,
|
|
637
655
|
description: meta.description,
|
|
@@ -766,6 +784,7 @@ export const defineApp = <
|
|
|
766
784
|
// Framework-owned mounts answer before the app's router ever runs, so the
|
|
767
785
|
// app middleware cannot report their template. Without this, every hashed
|
|
768
786
|
// island chunk would land in telemetry as its own route.
|
|
787
|
+
const invocationApp = { getApp: async (id: string) => (id === meta.id ? meta : null) };
|
|
769
788
|
const server = new Hono<AuthContext>()
|
|
770
789
|
.use("*", routeTemplate)
|
|
771
790
|
.get(APP_READINESS_PATH, () => appReadinessResponse(meta.id))
|
|
@@ -774,11 +793,14 @@ export const defineApp = <
|
|
|
774
793
|
.all("/public/*", servePublicAsset(isDevelopment, sharedStylesheetVersion(buildMetadata.release)))
|
|
775
794
|
.use(
|
|
776
795
|
"/_internal/sync/*",
|
|
777
|
-
requireInvocation(
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
796
|
+
requireInvocation(
|
|
797
|
+
(c) => ({
|
|
798
|
+
targetAppId: meta.id,
|
|
799
|
+
operation: syncInvocationOperation(c.req.method, new URL(c.req.url).pathname.slice("/_internal/sync".length)),
|
|
800
|
+
schemaHash: null,
|
|
801
|
+
}),
|
|
802
|
+
invocationApp,
|
|
803
|
+
),
|
|
782
804
|
)
|
|
783
805
|
.use("/_internal/sync/*", auth.requireOAuthScope("admin"))
|
|
784
806
|
.use("/_internal/sync/*", async (c, next) => {
|
|
@@ -787,79 +809,73 @@ export const defineApp = <
|
|
|
787
809
|
})
|
|
788
810
|
.route("/_internal/sync", createSyncOpsRoutes(getProcessSync));
|
|
789
811
|
|
|
790
|
-
if (opts.cli && cliModules.length > 0) server.route(CLOUD_CLI_PLUGIN_ROUTE, createCliPluginRoutes(meta.id, opts.cli));
|
|
791
|
-
|
|
792
|
-
if (compiledHelp) {
|
|
793
|
-
const pageBase = compiledHelp.summary.pageBase;
|
|
794
|
-
const centralPageBase = `/help/apps/${encodeURIComponent(meta.id)}`;
|
|
795
|
-
server.get(pageBase, (c) => c.redirect(centralPageBase, 302));
|
|
796
|
-
server.get(`${pageBase}/:topic`, (c) => c.redirect(`${centralPageBase}/${encodeURIComponent(c.req.param("topic"))}`, 302));
|
|
797
|
-
}
|
|
798
|
-
|
|
799
812
|
if (compiledCapabilities) {
|
|
800
|
-
const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
813
|
+
const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
|
|
814
|
+
withCapabilityRequestTimeout(c, kind, async () => {
|
|
815
|
+
// Framework-owned endpoints run before the app's settings middleware,
|
|
816
|
+
// so the operator default is read directly instead of via a snapshot.
|
|
817
|
+
const requestLocale = resolveLocale(c.req.raw.headers, await get<string>("app.locale"));
|
|
818
|
+
const messages = capabilityMessages(requestLocale);
|
|
819
|
+
const parsedBody = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
820
|
+
if (!parsedBody.ok) {
|
|
821
|
+
const message = parsedBody.reason === "too_large" ? messages.requestTooLarge : messages.requestBodyJson;
|
|
822
|
+
return c.json({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, 400);
|
|
823
|
+
}
|
|
824
|
+
if (
|
|
825
|
+
typeof parsedBody.data !== "object" ||
|
|
826
|
+
parsedBody.data === null ||
|
|
827
|
+
Array.isArray(parsedBody.data) ||
|
|
828
|
+
!Object.hasOwn(parsedBody.data, "input") ||
|
|
829
|
+
Object.keys(parsedBody.data).length !== 1
|
|
830
|
+
) {
|
|
831
|
+
return c.json(
|
|
832
|
+
{
|
|
833
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed,
|
|
834
|
+
message: messages.requestInputOnly,
|
|
835
|
+
},
|
|
836
|
+
400,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const body = parsedBody.data as { input: unknown };
|
|
840
|
+
const actor = c.get("actor");
|
|
841
|
+
const user = actor.kind === "user" ? actor.user : actor.delegatedUser;
|
|
842
|
+
const idempotencyKey = c.req.header("idempotency-key") || undefined;
|
|
843
|
+
const invocation = {
|
|
844
|
+
compiled: compiledCapabilities,
|
|
845
|
+
localId: c.req.param("capabilityId") ?? "",
|
|
846
|
+
input: body.input,
|
|
847
|
+
expectedSchemaHash: c.req.header("x-cloud-capability-schema-hash") ?? null,
|
|
848
|
+
context: {
|
|
849
|
+
authority: auth.getAuthority(c),
|
|
850
|
+
actor,
|
|
851
|
+
accessSubject: c.get("accessSubject"),
|
|
852
|
+
user,
|
|
853
|
+
idempotencyKey,
|
|
854
|
+
locale: requestLocale,
|
|
855
|
+
requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
|
|
856
|
+
origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
|
|
857
|
+
signal: c.req.raw.signal,
|
|
821
858
|
},
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
|
|
842
|
-
signal: c.req.raw.signal,
|
|
843
|
-
},
|
|
844
|
-
onUnexpectedError: (error: unknown) =>
|
|
845
|
-
log.error(kind === "review" ? "Capability review failed" : "Capability execution failed", {
|
|
846
|
-
appId: meta.id,
|
|
847
|
-
kind,
|
|
848
|
-
capabilityId: c.req.param("capabilityId") ?? "",
|
|
849
|
-
error: error instanceof Error ? error.message : String(error),
|
|
850
|
-
}),
|
|
851
|
-
};
|
|
852
|
-
const result =
|
|
853
|
-
kind === "review" ? await reviewCompiledCapability(invocation) : await invokeCompiledCapability({ ...invocation, kind });
|
|
854
|
-
const operation = kind === "action" ? compiledCapabilities.actions.get(invocation.localId) : undefined;
|
|
855
|
-
const serialized = serializeCapabilityProviderResult(result, {
|
|
856
|
-
nonIdempotentAction: kind === "action" && operation?.manifest.idempotency === "none",
|
|
857
|
-
});
|
|
858
|
-
return new Response(serialized.body, {
|
|
859
|
-
status: serialized.status,
|
|
860
|
-
headers: { "content-type": "application/json" },
|
|
859
|
+
onUnexpectedError: (error: unknown) =>
|
|
860
|
+
log.error(kind === "review" ? "Capability review failed" : "Capability execution failed", {
|
|
861
|
+
appId: meta.id,
|
|
862
|
+
kind,
|
|
863
|
+
capabilityId: c.req.param("capabilityId") ?? "",
|
|
864
|
+
error: error instanceof Error ? error.message : String(error),
|
|
865
|
+
}),
|
|
866
|
+
};
|
|
867
|
+
const result =
|
|
868
|
+
kind === "review" ? await reviewCompiledCapability(invocation) : await invokeCompiledCapability({ ...invocation, kind });
|
|
869
|
+
const operation = kind === "action" ? compiledCapabilities.actions.get(invocation.localId) : undefined;
|
|
870
|
+
const serialized = serializeCapabilityProviderResult(result, {
|
|
871
|
+
review: kind === "review",
|
|
872
|
+
nonIdempotentAction: kind === "action" && operation?.manifest.idempotency === "none",
|
|
873
|
+
});
|
|
874
|
+
return new Response(serialized.body, {
|
|
875
|
+
status: serialized.status,
|
|
876
|
+
headers: { "content-type": "application/json" },
|
|
877
|
+
});
|
|
861
878
|
});
|
|
862
|
-
};
|
|
863
879
|
const capabilityAuth = (kind: "queries" | "actions", review = false) =>
|
|
864
880
|
requireInvocation((c) => {
|
|
865
881
|
const localId = c.req.param("capabilityId") ?? "";
|
|
@@ -878,7 +894,7 @@ export const defineApp = <
|
|
|
878
894
|
operation: invocationOperation,
|
|
879
895
|
schemaHash: operation.manifest.schemaHash,
|
|
880
896
|
};
|
|
881
|
-
});
|
|
897
|
+
}, invocationApp);
|
|
882
898
|
for (const kind of ["queries", "actions"] as const) {
|
|
883
899
|
server.post(
|
|
884
900
|
`/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
|
|
@@ -893,6 +909,7 @@ export const defineApp = <
|
|
|
893
909
|
verb: c.req.param("verb") ?? "",
|
|
894
910
|
request: c.req.raw,
|
|
895
911
|
context: {
|
|
912
|
+
authority: auth.getAuthority(c),
|
|
896
913
|
actor,
|
|
897
914
|
accessSubject: c.get("accessSubject"),
|
|
898
915
|
user: actor.kind === "user" ? actor.user : actor.delegatedUser,
|
|
@@ -932,7 +949,7 @@ export const defineApp = <
|
|
|
932
949
|
const widgetId = c.req.param("widgetId") ?? "";
|
|
933
950
|
if (!declaredWidgetIds.has(widgetId) || !startOpts.widgets?.[widgetId]) return null;
|
|
934
951
|
return { targetAppId: meta.id, operation: widgetInvocationOperation(widgetId), schemaHash: null };
|
|
935
|
-
}),
|
|
952
|
+
}, invocationApp),
|
|
936
953
|
auth.requireOAuthScope("read", "admin"),
|
|
937
954
|
runtimeMiddleware(),
|
|
938
955
|
settingsMiddleware(),
|
|
@@ -947,10 +964,31 @@ export const defineApp = <
|
|
|
947
964
|
);
|
|
948
965
|
}
|
|
949
966
|
|
|
967
|
+
cleanup.push(() => stopAppAccessConnections());
|
|
968
|
+
server.use(
|
|
969
|
+
"*",
|
|
970
|
+
requireAppAccess(meta, (c, message) =>
|
|
971
|
+
ssr.error(c, 403, {
|
|
972
|
+
title: "No access",
|
|
973
|
+
description: message,
|
|
974
|
+
layout: "minimal",
|
|
975
|
+
}),
|
|
976
|
+
),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
if (opts.cli && cliModules.length > 0) server.route(CLOUD_CLI_PLUGIN_ROUTE, createCliPluginRoutes(meta.id, opts.cli));
|
|
980
|
+
|
|
981
|
+
if (compiledHelp) {
|
|
982
|
+
const pageBase = compiledHelp.summary.pageBase;
|
|
983
|
+
const centralPageBase = `/help/apps/${encodeURIComponent(meta.id)}`;
|
|
984
|
+
server.get(pageBase, (c) => c.redirect(centralPageBase, 302));
|
|
985
|
+
server.get(`${pageBase}/:topic`, (c) => c.redirect(`${centralPageBase}/${encodeURIComponent(c.req.param("topic"))}`, 302));
|
|
986
|
+
}
|
|
987
|
+
|
|
950
988
|
// OpenAPI spec mount. Registered on the framework server (before the
|
|
951
989
|
// user-fetch catch-all below) so it bypasses any auth / rate-limit
|
|
952
990
|
// middleware on the api router — the spec must stay reachable without
|
|
953
|
-
// a session for
|
|
991
|
+
// a session for legacy apps. Declared app access is enforced above.
|
|
954
992
|
//
|
|
955
993
|
// The `servers` override is load-bearing: hono-openapi walks the
|
|
956
994
|
// BARE api router and emits paths relative to its own root (e.g.
|
|
@@ -1030,18 +1068,36 @@ export const defineApp = <
|
|
|
1030
1068
|
log.info(`Registered "${meta.id}"`, { baseUrl });
|
|
1031
1069
|
|
|
1032
1070
|
// Graceful shutdown
|
|
1033
|
-
let
|
|
1034
|
-
const shutdown =
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1071
|
+
let shutdownPromise: Promise<void> | undefined;
|
|
1072
|
+
const shutdown = () =>
|
|
1073
|
+
(shutdownPromise ??= (async () => {
|
|
1074
|
+
const drained = http.stop();
|
|
1075
|
+
log.info(`Stopping: ${meta.id}`);
|
|
1076
|
+
try {
|
|
1077
|
+
await drained;
|
|
1078
|
+
} finally {
|
|
1079
|
+
await stopStartedResources();
|
|
1080
|
+
}
|
|
1081
|
+
})());
|
|
1040
1082
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1083
|
+
const stopAndExit = () =>
|
|
1084
|
+
void shutdown().then(
|
|
1085
|
+
() => process.exit(0),
|
|
1086
|
+
(error: unknown) => {
|
|
1087
|
+
log.error("HTTP shutdown failed", { appId: meta.id, error: error instanceof Error ? error.message : String(error) });
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
},
|
|
1090
|
+
);
|
|
1091
|
+
process.on("SIGTERM", stopAndExit);
|
|
1092
|
+
process.on("SIGINT", stopAndExit);
|
|
1043
1093
|
|
|
1044
|
-
return {
|
|
1094
|
+
return {
|
|
1095
|
+
hostname: "0.0.0.0",
|
|
1096
|
+
port,
|
|
1097
|
+
development: isDevelopment,
|
|
1098
|
+
idleTimeout: REQUEST_IDLE_TIMEOUT_SECONDS,
|
|
1099
|
+
fetch: (request, bunServer) => http.fetch(request, bunServer, (request) => server.fetch(request, bunServer)),
|
|
1100
|
+
};
|
|
1045
1101
|
} catch (error) {
|
|
1046
1102
|
await stopStartedResources();
|
|
1047
1103
|
throw error;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
|
|
3
|
+
/** Matches the existing runtime worker drain deadline. */
|
|
4
|
+
export const HTTP_DRAIN_TIMEOUT_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
/** Bun supplies the listening server to fetch, including for default-export entrypoints. */
|
|
7
|
+
export const createHttpDrain = (options: { timeoutMs?: number } = {}) => {
|
|
8
|
+
type HttpServer = Pick<Server<unknown>, "stop">;
|
|
9
|
+
const isHttpServer = (value: unknown): value is HttpServer =>
|
|
10
|
+
typeof value === "object" && value !== null && "stop" in value && typeof value.stop === "function";
|
|
11
|
+
const servers = new Set<HttpServer>();
|
|
12
|
+
let stopping = false;
|
|
13
|
+
let draining: Promise<void> | undefined;
|
|
14
|
+
return {
|
|
15
|
+
get stopping() {
|
|
16
|
+
return stopping;
|
|
17
|
+
},
|
|
18
|
+
fetch: async (request: Request, transport: unknown, handle: (request: Request) => Response | Promise<Response>): Promise<Response> => {
|
|
19
|
+
const server = isHttpServer(transport) ? transport : undefined;
|
|
20
|
+
if (server) servers.add(server);
|
|
21
|
+
if (stopping) {
|
|
22
|
+
// A listener that had not served a request before the signal was not
|
|
23
|
+
// captured yet. Reject that first request and close it as well.
|
|
24
|
+
void server?.stop(false);
|
|
25
|
+
return Response.json({ status: "stopping" }, { status: 503, headers: { "Retry-After": "3", Connection: "close" } });
|
|
26
|
+
}
|
|
27
|
+
// Preserve Bun request identity for websocket upgrades, requestIP and
|
|
28
|
+
// per-request timeouts. Forced stop aborts the native request signal.
|
|
29
|
+
return handle(request);
|
|
30
|
+
},
|
|
31
|
+
stop: (): Promise<void> => {
|
|
32
|
+
stopping = true;
|
|
33
|
+
draining ??= (async () => {
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
try {
|
|
37
|
+
await Promise.race([
|
|
38
|
+
Promise.all([...servers].map((server) => server.stop(false))),
|
|
39
|
+
new Promise<void>((resolve) => {
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
// Cancel application work as well as the transport, before
|
|
42
|
+
// lifecycle hooks can tear down NATS or other dependencies.
|
|
43
|
+
timedOut = true;
|
|
44
|
+
resolve();
|
|
45
|
+
}, options.timeoutMs ?? HTTP_DRAIN_TIMEOUT_MS);
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
if (timedOut) await Promise.all([...servers].map((server) => server.stop(true)));
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return draining;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLOUD_CLI_MODULE_NAME } from "../cli/plugin";
|
|
2
|
+
import { AppAccessRuleSchema } from "../contracts/app-access";
|
|
2
3
|
import { PlatformPermissionSchema } from "../contracts/outgoing-mail";
|
|
3
4
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
4
5
|
import { isOwnAppPath, SAME_ORIGIN_PATH } from "./app-path";
|
|
@@ -28,6 +29,8 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
28
29
|
if (!isStringArray(value.routes) || value.routes.some((route) => !route.startsWith("/"))) {
|
|
29
30
|
return invalid("routes", "an array of absolute paths");
|
|
30
31
|
}
|
|
32
|
+
if (value.access !== undefined && !AppAccessRuleSchema.safeParse(value.access).success)
|
|
33
|
+
return invalid("access", "a declarative app access rule");
|
|
31
34
|
if (value.startedAt !== undefined && !(typeof value.startedAt === "number" && Number.isFinite(value.startedAt))) {
|
|
32
35
|
return invalid("startedAt", "a finite number");
|
|
33
36
|
}
|
|
@@ -15,6 +15,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
15
15
|
const searchTags = searchQueries?.flatMap((query) => query.universalSearch!.tags.flatMap((tag) => [tag.tag, ...(tag.aliases ?? [])]));
|
|
16
16
|
return {
|
|
17
17
|
id: e.id,
|
|
18
|
+
access: e.access,
|
|
18
19
|
platformPermissions: e.platformPermissions ? [...e.platformPermissions] : undefined,
|
|
19
20
|
name: e.name,
|
|
20
21
|
icon: e.icon,
|
|
@@ -7,6 +7,7 @@ import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
|
7
7
|
import type { SettingsFlat, Unflatten } from "../contracts/settings-types";
|
|
8
8
|
import type { User } from "../contracts/shared";
|
|
9
9
|
import type { CORE_SETTINGS } from "../services/settings/core-settings";
|
|
10
|
+
import { preloadDisplayApps } from "../ssr/runtime";
|
|
10
11
|
import { measureServerPhase } from "./server-timing";
|
|
11
12
|
|
|
12
13
|
type PageEnv<T extends object> = {
|
|
@@ -45,6 +46,7 @@ export const createStatusPreservingSsrHandler = <T extends object>(
|
|
|
45
46
|
const handler = args[args.length - 1] as SsrHandler<E, T>;
|
|
46
47
|
|
|
47
48
|
return createHandler<E>(...middlewares, async (context) => {
|
|
49
|
+
await preloadDisplayApps(context);
|
|
48
50
|
const result = await measureServerPhase(context, "ssr_data", () => handler(context as Context<E & PageEnv<T>>));
|
|
49
51
|
if (result instanceof Response) return result;
|
|
50
52
|
await measureServerPhase(context, "ssr_finalize", () => finalizePage?.(context as unknown as Context<PageEnv<T>>));
|
|
@@ -4,11 +4,11 @@ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "so
|
|
|
4
4
|
import { createStore, reconcile } from "solid-js/store";
|
|
5
5
|
import { CloudAvatar } from "../account/Avatar";
|
|
6
6
|
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
|
|
7
|
-
import { groupDisplayName } from "../shared/account-display";
|
|
8
7
|
import { createGroupCoverage, type GroupCoverage, GroupCoveragePanel, GroupCoverageToggle } from "./GroupCoverage";
|
|
9
8
|
import { isManagerEntry } from "./managers";
|
|
10
9
|
import { accessMessages } from "./messages";
|
|
11
10
|
import PrincipalPicker, { principalKey } from "./PrincipalPicker";
|
|
11
|
+
import { principalLabel } from "./principal-label";
|
|
12
12
|
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
13
13
|
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -43,7 +43,7 @@ type PermissionEditorProps = {
|
|
|
43
43
|
grantAccess: (
|
|
44
44
|
principal: Principal,
|
|
45
45
|
permission: GrantableLevel,
|
|
46
|
-
display?: { displayName: string; serviceAccountKind?: ServiceAccountKind },
|
|
46
|
+
display?: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
|
|
47
47
|
scope?: string,
|
|
48
48
|
) => Promise<AccessEntry>;
|
|
49
49
|
|
|
@@ -145,16 +145,6 @@ const resolveEntryDisplay = (
|
|
|
145
145
|
return defaults[permission] ?? defaults.none;
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
const getEntryDisplayName = (entry: AccessEntry, t: ReturnType<typeof accessMessages.resolve>["t"], locale: string): string => {
|
|
149
|
-
// Audiences always use the localized label; a server-supplied name is not translated.
|
|
150
|
-
if (entry.principal.type === "authenticated") return t.allUsers;
|
|
151
|
-
if (entry.principal.type === "public") return t.public;
|
|
152
|
-
if (entry.displayName) return entry.principal.type === "group" ? groupDisplayName(entry.displayName, locale) : entry.displayName;
|
|
153
|
-
if (entry.principal.type === "user") return entry.principal.userId;
|
|
154
|
-
if (entry.principal.type === "service_account") return entry.principal.serviceAccountId;
|
|
155
|
-
return entry.principal.groupId;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
148
|
const getPrincipalIcon = (entry: AccessEntry, t: ReturnType<typeof accessMessages.resolve>["t"]): string => {
|
|
159
149
|
switch (entry.principal.type) {
|
|
160
150
|
case "user":
|
|
@@ -204,7 +194,10 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
204
194
|
}
|
|
205
195
|
}
|
|
206
196
|
|
|
207
|
-
const grant = async (
|
|
197
|
+
const grant = async (
|
|
198
|
+
principal: Principal,
|
|
199
|
+
display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
|
|
200
|
+
) => {
|
|
208
201
|
if (disposed || grantPending() || updateMut.loading() || revokeMut.loading()) return;
|
|
209
202
|
const key = principalKey(principal);
|
|
210
203
|
if (entries.some((entry) => principalKey(entry.principal) === key)) return;
|
|
@@ -224,8 +217,14 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
224
217
|
const entry = await props.grantAccess(principal, first.level, display, first.scope);
|
|
225
218
|
if (disposed) return;
|
|
226
219
|
const index = entries.findIndex((entry) => entry.id === id);
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
const saved = {
|
|
221
|
+
...entry,
|
|
222
|
+
displayName: entry.displayName ?? display.displayName,
|
|
223
|
+
avatarHash: entry.avatarHash !== undefined ? entry.avatarHash : display.avatarHash,
|
|
224
|
+
serviceAccountKind: entry.serviceAccountKind ?? display.serviceAccountKind,
|
|
225
|
+
};
|
|
226
|
+
if (index !== -1) setEntries(index, reconcile(saved));
|
|
227
|
+
announce(t().accessGranted({ name: principalLabel(saved, locale()) }));
|
|
229
228
|
} catch (err) {
|
|
230
229
|
if (!disposed) setEntries((current) => current.filter((entry) => entry.id !== id));
|
|
231
230
|
// Prompts are global, so a grant that fails after its dialog closed still says so.
|
|
@@ -264,7 +263,7 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
264
263
|
|
|
265
264
|
const revokeMut = mutation.create<string | null, AccessEntry>({
|
|
266
265
|
mutation: async (entry) => {
|
|
267
|
-
const displayName =
|
|
266
|
+
const displayName = principalLabel(entry, locale());
|
|
268
267
|
const confirmed = await prompts.confirm(t().removeAccessConfirm({ name: displayName }), {
|
|
269
268
|
title: t().removeAccess,
|
|
270
269
|
variant: "danger",
|
|
@@ -395,7 +394,7 @@ function AccessEntryRow(props: {
|
|
|
395
394
|
}) {
|
|
396
395
|
const locale = useLocale();
|
|
397
396
|
const t = () => accessMessages.resolve([locale()]).t;
|
|
398
|
-
const displayName = () =>
|
|
397
|
+
const displayName = () => principalLabel(props.entry, locale());
|
|
399
398
|
const entryKey = () => levelKey(props.entry.permission, props.entry.scope);
|
|
400
399
|
const display = () => resolveEntryDisplay(props.entry.permission, props.entry.scope, props.allowed, t());
|
|
401
400
|
const manageLabel = () => resolveEntryDisplay("admin", undefined, props.allowed, t()).label;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Combobox, type ComboboxOption, useLocale } from "@k2b/ui";
|
|
2
2
|
import type { Principal, ServiceAccountKind } from "../contracts/shared";
|
|
3
|
-
import { groupDisplayName } from "../shared/account-display";
|
|
4
3
|
import { accessMessages } from "./messages";
|
|
4
|
+
import { principalLabel } from "./principal-label";
|
|
5
5
|
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
6
6
|
|
|
7
7
|
export const principalKey = (p: Principal) =>
|
|
@@ -13,7 +13,7 @@ export const principalKey = (p: Principal) =>
|
|
|
13
13
|
? `s:${p.serviceAccountId}`
|
|
14
14
|
: p.type;
|
|
15
15
|
type Entity =
|
|
16
|
-
| { kind: "user"; user: { id: string; displayName: string; uid: string; mail: string | null } }
|
|
16
|
+
| { kind: "user"; user: { id: string; displayName: string; uid: string; mail: string | null; avatarHash?: string | null } }
|
|
17
17
|
| { kind: "group"; group: { id: string; name: string; description: string | null } }
|
|
18
18
|
| {
|
|
19
19
|
kind: "service_account";
|
|
@@ -35,17 +35,36 @@ export default function PrincipalPicker(props: {
|
|
|
35
35
|
disabled?: boolean;
|
|
36
36
|
readOnly?: boolean;
|
|
37
37
|
/** `serviceAccountKind` is set for service accounts so a new row can show the same kind as the result. */
|
|
38
|
-
onSelect: (
|
|
38
|
+
onSelect: (
|
|
39
|
+
principal: Principal,
|
|
40
|
+
display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
|
|
41
|
+
) => void;
|
|
39
42
|
}) {
|
|
40
43
|
const locale = useLocale(),
|
|
41
44
|
t = () => accessMessages.resolve([locale()]).t;
|
|
42
|
-
type Entry = {
|
|
45
|
+
type Entry = {
|
|
46
|
+
principal: Principal;
|
|
47
|
+
label: string;
|
|
48
|
+
icon: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
avatarHash?: string | null;
|
|
51
|
+
serviceAccountKind?: ServiceAccountKind;
|
|
52
|
+
};
|
|
43
53
|
let selectable = new Map<string, Entry>();
|
|
44
54
|
const load = async (search: string, signal: AbortSignal): Promise<ComboboxOption[]> => {
|
|
45
55
|
const entries: Entry[] = [];
|
|
46
56
|
if (props.allowAuthenticated !== false)
|
|
47
|
-
entries.push({
|
|
48
|
-
|
|
57
|
+
entries.push({
|
|
58
|
+
principal: { type: "authenticated" },
|
|
59
|
+
label: principalLabel({ principal: { type: "authenticated" } }, locale()),
|
|
60
|
+
icon: "ti ti-lock-open-2",
|
|
61
|
+
});
|
|
62
|
+
if (props.allowPublic)
|
|
63
|
+
entries.push({
|
|
64
|
+
principal: { type: "public" },
|
|
65
|
+
label: principalLabel({ principal: { type: "public" } }, locale()),
|
|
66
|
+
icon: "ti ti-world",
|
|
67
|
+
});
|
|
49
68
|
if (search.length >= 2) {
|
|
50
69
|
const url = new URL("/api/accounts/entities", window.location.origin);
|
|
51
70
|
url.searchParams.set("search", search);
|
|
@@ -64,14 +83,18 @@ export default function PrincipalPicker(props: {
|
|
|
64
83
|
if (e.kind === "user")
|
|
65
84
|
entries.push({
|
|
66
85
|
principal: { type: "user", userId: e.user.id },
|
|
67
|
-
label:
|
|
86
|
+
label: principalLabel(
|
|
87
|
+
{ principal: { type: "user", userId: e.user.id }, displayName: e.user.displayName || e.user.uid },
|
|
88
|
+
locale(),
|
|
89
|
+
),
|
|
90
|
+
avatarHash: e.user.avatarHash,
|
|
68
91
|
icon: "ti ti-user",
|
|
69
92
|
description: e.user.mail ?? e.user.uid,
|
|
70
93
|
});
|
|
71
94
|
else if (e.kind === "group")
|
|
72
95
|
entries.push({
|
|
73
96
|
principal: { type: "group", groupId: e.group.id },
|
|
74
|
-
label:
|
|
97
|
+
label: principalLabel({ principal: { type: "group", groupId: e.group.id }, displayName: e.group.name }, locale()),
|
|
75
98
|
icon: "ti ti-users-group",
|
|
76
99
|
description: e.group.description ?? undefined,
|
|
77
100
|
});
|
|
@@ -80,7 +103,14 @@ export default function PrincipalPicker(props: {
|
|
|
80
103
|
const kind = serviceAccountKindDisplay(account.kind, t());
|
|
81
104
|
entries.push({
|
|
82
105
|
principal: { type: "service_account", serviceAccountId: account.id },
|
|
83
|
-
label:
|
|
106
|
+
label: principalLabel(
|
|
107
|
+
{
|
|
108
|
+
principal: { type: "service_account", serviceAccountId: account.id },
|
|
109
|
+
displayName: account.name,
|
|
110
|
+
serviceAccountKind: account.kind,
|
|
111
|
+
},
|
|
112
|
+
locale(),
|
|
113
|
+
),
|
|
84
114
|
// Resource-bound accounts often share a name; their binding tells them apart.
|
|
85
115
|
description:
|
|
86
116
|
account.kind === "resource_bound"
|
|
@@ -114,7 +144,12 @@ export default function PrincipalPicker(props: {
|
|
|
114
144
|
fetchData={load}
|
|
115
145
|
onSelect={(option) => {
|
|
116
146
|
const entry = selectable.get(option.id);
|
|
117
|
-
if (entry)
|
|
147
|
+
if (entry)
|
|
148
|
+
props.onSelect(entry.principal, {
|
|
149
|
+
displayName: entry.label,
|
|
150
|
+
avatarHash: entry.avatarHash,
|
|
151
|
+
serviceAccountKind: entry.serviceAccountKind,
|
|
152
|
+
});
|
|
118
153
|
}}
|
|
119
154
|
/>
|
|
120
155
|
);
|