@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
|
@@ -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);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { buildMetadata } from "./build-metadata";
|
|
2
|
+
|
|
1
3
|
export const APP_READINESS_PATH = "/_cloud/ready";
|
|
2
4
|
|
|
3
|
-
export const appReadinessResponse = (appId: string): Response =>
|
|
5
|
+
export const appReadinessResponse = (appId: string): Response =>
|
|
6
|
+
Response.json({ status: "ready", appId, version: buildMetadata.version, release: buildMetadata.release });
|
|
@@ -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
|
+
}
|
|
@@ -2,10 +2,20 @@ import type { HtmlFn, RenderFn } from "@k2b/ssr";
|
|
|
2
2
|
import { createSSRHandler } from "@k2b/ssr/hono";
|
|
3
3
|
import type { Context, Env, MiddlewareHandler, TypedResponse } from "hono";
|
|
4
4
|
import type { StatusCode } from "hono/utils/http-status";
|
|
5
|
-
import type {
|
|
5
|
+
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
6
|
+
import type { SettingsFlat, Unflatten } from "../contracts/settings-types";
|
|
6
7
|
import type { User } from "../contracts/shared";
|
|
8
|
+
import type { CORE_SETTINGS } from "../services/settings/core-settings";
|
|
9
|
+
import { measureServerPhase } from "./server-timing";
|
|
7
10
|
|
|
8
|
-
type PageEnv<T extends object> = {
|
|
11
|
+
type PageEnv<T extends object> = {
|
|
12
|
+
Variables: {
|
|
13
|
+
page: Partial<T>;
|
|
14
|
+
user?: User;
|
|
15
|
+
railPreferences?: RailSnapshot;
|
|
16
|
+
settings?: Unflatten<SettingsFlat<typeof CORE_SETTINGS>>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
9
19
|
type SsrHandlerResult = RenderFn | Response | TypedResponse;
|
|
10
20
|
type SsrHandler<E extends Env, T extends object> = (context: Context<E & PageEnv<T>>) => SsrHandlerResult | Promise<SsrHandlerResult>;
|
|
11
21
|
|
|
@@ -30,15 +40,17 @@ export const createStatusPreservingSsrHandler = <T extends object>(
|
|
|
30
40
|
const handler = args[args.length - 1] as SsrHandler<E, T>;
|
|
31
41
|
|
|
32
42
|
return createHandler<E>(...middlewares, async (context) => {
|
|
33
|
-
const result = await handler(context as Context<E & PageEnv<T>>);
|
|
43
|
+
const result = await measureServerPhase(context, "ssr_data", () => handler(context as Context<E & PageEnv<T>>));
|
|
34
44
|
if (result instanceof Response) return result;
|
|
35
|
-
await finalizePage?.(context as unknown as Context<PageEnv<T>>);
|
|
45
|
+
await measureServerPhase(context, "ssr_finalize", () => finalizePage?.(context as unknown as Context<PageEnv<T>>));
|
|
36
46
|
if (typeof result !== "function") return result;
|
|
37
47
|
|
|
38
48
|
const status = context.newResponse(null).status;
|
|
39
|
-
if (
|
|
49
|
+
if (result.constructor.name === "AsyncFunction") {
|
|
50
|
+
throw new Error("[ssr] ssr() render functions must be synchronous: return () => <Page />");
|
|
51
|
+
}
|
|
40
52
|
|
|
41
|
-
const response = await html(result, context.get("page") as T);
|
|
53
|
+
const response = await measureServerPhase(context, "ssr_render", () => html(result, context.get("page") as T));
|
|
42
54
|
const headers: Record<string, string> = {};
|
|
43
55
|
response.headers.forEach((value, key) => {
|
|
44
56
|
headers[key] = value;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { dependencies } from "../../package.json";
|
|
3
|
+
import * as collectorModule from "../browser/web-vitals.ts" with { type: "text" };
|
|
4
|
+
|
|
5
|
+
// All apps using the same collector share one Core asset and browser cache key.
|
|
6
|
+
// Both inputs are bundled, so production servers need no source files on disk.
|
|
7
|
+
if (!("default" in collectorModule) || typeof collectorModule.default !== "string") throw new Error("Expected collector source text");
|
|
8
|
+
const version = createHash("sha256").update(collectorModule.default).update(dependencies["web-vitals"]).digest("hex").slice(0, 16);
|
|
9
|
+
export const WEB_VITALS_ASSET_NAME = `web-vitals-${version}.js`;
|
|
10
|
+
export const WEB_VITALS_ASSET_HREF = `/public/${WEB_VITALS_ASSET_NAME}`;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { mutation } from "@k2b/stdlib/solid";
|
|
2
|
-
import {
|
|
2
|
+
import { IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
|
|
3
3
|
import { createSignal, For, Show } from "solid-js";
|
|
4
4
|
import { CloudAvatar } from "../account/Avatar";
|
|
5
5
|
import type { AccessEntry, PermissionLevel, Principal } from "../contracts/shared";
|
|
6
6
|
import { accessMessages } from "./messages";
|
|
7
|
+
import PrincipalPicker from "./PrincipalPicker";
|
|
7
8
|
|
|
8
9
|
// ─────────────────────────────────────────────────────────────────────────
|
|
9
10
|
// Public API
|
|
@@ -56,8 +57,8 @@ type PermissionEditorProps = {
|
|
|
56
57
|
* write "Use", views call read "View"). When undefined, all three
|
|
57
58
|
* are offered with default labels. New entries are granted
|
|
58
59
|
* `allowedLevels[0]` on pick — the user upgrades via the row pill
|
|
59
|
-
* afterwards. */
|
|
60
|
-
allowedLevels?: AllowedLevel[];
|
|
60
|
+
* afterwards. A function may restrict levels by principal (for example public read-only). */
|
|
61
|
+
allowedLevels?: AllowedLevel[] | ((principal: Principal) => AllowedLevel[]);
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -135,22 +136,6 @@ const getPrincipalIcon = (principal: Principal): string => {
|
|
|
135
136
|
}
|
|
136
137
|
};
|
|
137
138
|
|
|
138
|
-
// Backend `/api/accounts/entities` shape (the subset we consume).
|
|
139
|
-
type ApiEntity =
|
|
140
|
-
| { kind: "user"; user: { id: string; uid: string; displayName: string; mail: string | null } }
|
|
141
|
-
| { kind: "group"; group: { id: string; name: string; description: string | null } }
|
|
142
|
-
| {
|
|
143
|
-
kind: "service_account";
|
|
144
|
-
serviceAccount: {
|
|
145
|
-
id: string;
|
|
146
|
-
name: string;
|
|
147
|
-
kind: "user_delegated" | "resource_bound";
|
|
148
|
-
appId: string | null;
|
|
149
|
-
resourceType: string | null;
|
|
150
|
-
resourceId: string | null;
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
|
|
154
139
|
// ─────────────────────────────────────────────────────────────────────────
|
|
155
140
|
// PermissionEditor
|
|
156
141
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -162,12 +147,12 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
162
147
|
const canEdit = () => props.canEdit !== false;
|
|
163
148
|
const allowPublic = () => props.allowPublic === true;
|
|
164
149
|
const allowAuthenticated = () => props.allowAuthenticated !== false;
|
|
165
|
-
const allowed = () =>
|
|
166
|
-
|
|
150
|
+
const allowed = (principal: Principal) =>
|
|
151
|
+
resolveAllowedLevels(typeof props.allowedLevels === "function" ? props.allowedLevels(principal) : props.allowedLevels, t());
|
|
167
152
|
|
|
168
153
|
// Defensive dev-warning: an empty allowedLevels array makes the editor
|
|
169
154
|
// unable to grant anything.
|
|
170
|
-
if (props.allowedLevels && props.allowedLevels.length === 0) {
|
|
155
|
+
if (Array.isArray(props.allowedLevels) && props.allowedLevels.length === 0) {
|
|
171
156
|
if (typeof console !== "undefined") {
|
|
172
157
|
console.warn(
|
|
173
158
|
"[PermissionEditor] `allowedLevels=[]` — the editor cannot grant any permission. Pass at least one level or omit the prop for the default View / Edit / Manage set.",
|
|
@@ -175,21 +160,6 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
175
160
|
}
|
|
176
161
|
}
|
|
177
162
|
|
|
178
|
-
const existingUserIds = () =>
|
|
179
|
-
entries()
|
|
180
|
-
.filter((e) => e.principal.type === "user")
|
|
181
|
-
.map((e) => (e.principal as { type: "user"; userId: string }).userId);
|
|
182
|
-
const existingGroupIds = () =>
|
|
183
|
-
entries()
|
|
184
|
-
.filter((e) => e.principal.type === "group")
|
|
185
|
-
.map((e) => (e.principal as { type: "group"; groupId: string }).groupId);
|
|
186
|
-
const existingServiceAccountIds = () =>
|
|
187
|
-
entries()
|
|
188
|
-
.filter((e) => e.principal.type === "service_account")
|
|
189
|
-
.map((e) => (e.principal as { type: "service_account"; serviceAccountId: string }).serviceAccountId);
|
|
190
|
-
const hasAuthenticatedEntry = () => entries().some((entry) => entry.principal.type === "authenticated");
|
|
191
|
-
const hasPublicEntry = () => entries().some((entry) => entry.principal.type === "public");
|
|
192
|
-
|
|
193
163
|
const grantMut = mutation.create({
|
|
194
164
|
mutation: async (data: { principal: Principal; permission: GrantableLevel; display: { displayName: string } }) =>
|
|
195
165
|
props.grantAccess(data.principal, data.permission, data.display),
|
|
@@ -230,106 +200,6 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
230
200
|
});
|
|
231
201
|
const busy = () => grantMut.loading() || updateMut.loading() || revokeMut.loading();
|
|
232
202
|
|
|
233
|
-
// ── Combobox add-flow ─────────────────────────────────────────────────
|
|
234
|
-
// The Combobox is a fire-and-forget input: type → pick → granted at the
|
|
235
|
-
// lowest allowed level. The `principalsByOptId` map carries the original
|
|
236
|
-
// discriminated principal across the ComboboxOption boundary so onSelect
|
|
237
|
-
// can route it to grantAccess without re-parsing prefixed ids.
|
|
238
|
-
let principalsByOptId = new Map<string, Principal>();
|
|
239
|
-
|
|
240
|
-
const fetchPrincipals = async (q: string, signal: AbortSignal): Promise<ComboboxOption[]> => {
|
|
241
|
-
const map = new Map<string, Principal>();
|
|
242
|
-
const opts: ComboboxOption[] = [];
|
|
243
|
-
|
|
244
|
-
// Synthetic principals — only when allowed AND not already granted.
|
|
245
|
-
// Placed first so they're visible immediately on focus, before any
|
|
246
|
-
// typing kicks off a backend request.
|
|
247
|
-
if (allowAuthenticated() && !hasAuthenticatedEntry()) {
|
|
248
|
-
map.set("auth", { type: "authenticated" });
|
|
249
|
-
opts.push({
|
|
250
|
-
id: "auth",
|
|
251
|
-
label: t().allUsers,
|
|
252
|
-
description: t().signedInDescription,
|
|
253
|
-
icon: "ti-lock-open-2",
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
if (allowPublic() && !hasPublicEntry()) {
|
|
257
|
-
map.set("public", { type: "public" });
|
|
258
|
-
opts.push({
|
|
259
|
-
id: "public",
|
|
260
|
-
label: t().public,
|
|
261
|
-
description: t().publicDescription,
|
|
262
|
-
icon: "ti-world",
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Real entities require a query — avoid a wide listing on every focus.
|
|
267
|
-
if (q.length >= 2) {
|
|
268
|
-
const url = new URL("/api/accounts/entities", window.location.origin);
|
|
269
|
-
url.searchParams.set("search", q);
|
|
270
|
-
url.searchParams.set("kinds", props.allowServiceAccounts ? "user,group,service_account" : "user,group");
|
|
271
|
-
url.searchParams.set("per_page", "10");
|
|
272
|
-
const userIds = existingUserIds();
|
|
273
|
-
if (userIds.length) url.searchParams.set("exclude_user_ids", userIds.join(","));
|
|
274
|
-
const groupIds = existingGroupIds();
|
|
275
|
-
if (groupIds.length) url.searchParams.set("exclude_group_ids", groupIds.join(","));
|
|
276
|
-
const serviceAccountIds = existingServiceAccountIds();
|
|
277
|
-
if (serviceAccountIds.length) url.searchParams.set("exclude_service_account_ids", serviceAccountIds.join(","));
|
|
278
|
-
|
|
279
|
-
const res = await fetch(url.toString(), { credentials: "same-origin", signal });
|
|
280
|
-
if (res.ok) {
|
|
281
|
-
const data = (await res.json()) as { items?: ApiEntity[] };
|
|
282
|
-
for (const item of data.items ?? []) {
|
|
283
|
-
if (item.kind === "user") {
|
|
284
|
-
const id = `u:${item.user.id}`;
|
|
285
|
-
map.set(id, { type: "user", userId: item.user.id });
|
|
286
|
-
opts.push({
|
|
287
|
-
id,
|
|
288
|
-
label: item.user.displayName,
|
|
289
|
-
description: item.user.mail ?? item.user.uid,
|
|
290
|
-
icon: "ti ti-user",
|
|
291
|
-
});
|
|
292
|
-
} else if (item.kind === "group") {
|
|
293
|
-
const id = `g:${item.group.id}`;
|
|
294
|
-
map.set(id, { type: "group", groupId: item.group.id });
|
|
295
|
-
opts.push({
|
|
296
|
-
id,
|
|
297
|
-
label: item.group.name,
|
|
298
|
-
description: item.group.description ?? undefined,
|
|
299
|
-
icon: "ti ti-users-group",
|
|
300
|
-
});
|
|
301
|
-
} else if (item.kind === "service_account") {
|
|
302
|
-
const id = `sa:${item.serviceAccount.id}`;
|
|
303
|
-
map.set(id, { type: "service_account", serviceAccountId: item.serviceAccount.id });
|
|
304
|
-
opts.push({
|
|
305
|
-
id,
|
|
306
|
-
label: item.serviceAccount.name,
|
|
307
|
-
description:
|
|
308
|
-
item.serviceAccount.kind === "user_delegated"
|
|
309
|
-
? t().userBoundServiceAccount
|
|
310
|
-
: [item.serviceAccount.appId, item.serviceAccount.resourceType, item.serviceAccount.resourceId]
|
|
311
|
-
.filter(Boolean)
|
|
312
|
-
.join(" · "),
|
|
313
|
-
icon: "ti ti-key",
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
principalsByOptId = map;
|
|
321
|
-
return opts;
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const handleSelect = (option: ComboboxOption) => {
|
|
325
|
-
if (busy()) return;
|
|
326
|
-
const principal = principalsByOptId.get(option.id);
|
|
327
|
-
if (!principal) return;
|
|
328
|
-
const firstLevel = allowed()[0]?.level;
|
|
329
|
-
if (!firstLevel) return; // dev-warned above; bail silently
|
|
330
|
-
grantMut.mutate({ principal, permission: firstLevel, display: { displayName: option.label } });
|
|
331
|
-
};
|
|
332
|
-
|
|
333
203
|
return (
|
|
334
204
|
<div class="flex flex-col gap-3">
|
|
335
205
|
{/* Existing entries */}
|
|
@@ -340,8 +210,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
340
210
|
entry={entry}
|
|
341
211
|
canEdit={canEdit()}
|
|
342
212
|
disabled={busy()}
|
|
343
|
-
allowed={allowed()}
|
|
344
|
-
singlePicker={
|
|
213
|
+
allowed={allowed(entry.principal)}
|
|
214
|
+
singlePicker={allowed(entry.principal).length === 1}
|
|
345
215
|
onUpdatePermission={(permission) => {
|
|
346
216
|
if (!busy()) void updateMut.mutate({ accessId: entry.id, permission });
|
|
347
217
|
}}
|
|
@@ -360,19 +230,16 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
360
230
|
level on pick. The user upgrades via the row pill if they want
|
|
361
231
|
a higher level. KISS: one decision per step. */}
|
|
362
232
|
<Show when={canEdit()}>
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
: t().addService
|
|
369
|
-
: allowAuthenticated()
|
|
370
|
-
? t().addAudience
|
|
371
|
-
: t().addBasic
|
|
372
|
-
}
|
|
373
|
-
fetchData={fetchPrincipals}
|
|
374
|
-
onSelect={handleSelect}
|
|
233
|
+
<PrincipalPicker
|
|
234
|
+
existing={entries().map((e) => e.principal)}
|
|
235
|
+
allowPublic={allowPublic()}
|
|
236
|
+
allowAuthenticated={allowAuthenticated()}
|
|
237
|
+
allowServiceAccounts={props.allowServiceAccounts}
|
|
375
238
|
disabled={busy()}
|
|
239
|
+
onSelect={(principal, display) => {
|
|
240
|
+
const permission = allowed(principal)[0]?.level;
|
|
241
|
+
if (permission && !busy()) grantMut.mutate({ principal, permission, display });
|
|
242
|
+
}}
|
|
376
243
|
/>
|
|
377
244
|
</Show>
|
|
378
245
|
</div>
|