@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,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Commands carry small public references/options, never document bodies or secrets. */
|
|
4
|
+
export const COMMAND_MAX_URL_LENGTH = 8192;
|
|
5
|
+
export const CommandPathSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.max(2000)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
if (!value.startsWith("/") || /[\\\u0000-\u0020]/u.test(value)) return false;
|
|
10
|
+
try {
|
|
11
|
+
return new URL(value, "https://cloud.invalid").origin === "https://cloud.invalid";
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}, "Expected a Cloud path");
|
|
16
|
+
|
|
17
|
+
export const CommandOptionsSchema = z.object({ returnTo: CommandPathSchema.optional() }).strict();
|
|
18
|
+
export type CommandOptions = z.infer<typeof CommandOptionsSchema>;
|
|
19
|
+
export const CommandRequestSchema = CommandOptionsSchema.extend({ input: z.unknown() }).strict();
|
|
20
|
+
export const CommandLinkSchema = z.object({ href: z.url().max(COMMAND_MAX_URL_LENGTH) }).strict();
|
|
21
|
+
|
|
22
|
+
export const commandPath = (path: string, command: string, input: unknown, options: CommandOptions = {}): string => {
|
|
23
|
+
const url = new URL(CommandPathSchema.parse(path), "https://cloud.invalid");
|
|
24
|
+
url.searchParams.set("command", command);
|
|
25
|
+
url.searchParams.set("commandInput", JSON.stringify(input));
|
|
26
|
+
const parsed = CommandOptionsSchema.parse(options);
|
|
27
|
+
if (parsed.returnTo) url.searchParams.set("commandReturn", parsed.returnTo);
|
|
28
|
+
else url.searchParams.delete("commandReturn");
|
|
29
|
+
const result = `${url.pathname}${url.search}${url.hash}`;
|
|
30
|
+
if (result.length > COMMAND_MAX_URL_LENGTH) throw new Error("Command link is too large");
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const readCommand = (url: URL) => {
|
|
35
|
+
const id = url.searchParams.get("command");
|
|
36
|
+
if (!id) return null;
|
|
37
|
+
const raw = url.searchParams.get("commandInput") ?? "{}";
|
|
38
|
+
if (url.href.length > COMMAND_MAX_URL_LENGTH) throw new Error("Command link is too large");
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
input: JSON.parse(raw) as unknown,
|
|
42
|
+
options: CommandOptionsSchema.parse({ returnTo: url.searchParams.get("commandReturn") ?? undefined }),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const clearCommand = (url: URL): string => {
|
|
47
|
+
for (const key of ["command", "commandInput", "commandReturn"]) url.searchParams.delete(key);
|
|
48
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
49
|
+
};
|
package/src/contracts/index.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export * from "./account-categories";
|
|
2
2
|
export * from "./announcements";
|
|
3
3
|
export * from "./app";
|
|
4
|
+
export * from "./app-approval";
|
|
4
5
|
export * from "./capabilities";
|
|
5
|
-
export * from "./
|
|
6
|
+
export * from "./commands";
|
|
6
7
|
export * from "./notification-types";
|
|
7
8
|
export * from "./posix";
|
|
8
9
|
export * from "./profile";
|
|
10
|
+
// Core app bar administration and shared shortcut values.
|
|
11
|
+
export { RailAccessSchema, type RailAdminEntry, RailAdminInputSchema, RailAdminSchema, type RailAdminState } from "./rail-admin";
|
|
12
|
+
export type { RailShortcut } from "./rail-preferences";
|
|
9
13
|
export * from "./registry";
|
|
10
14
|
export * from "./resource-clipboard";
|
|
11
15
|
export * from "./shared";
|
|
12
16
|
export * from "./user-notifications";
|
|
13
17
|
export * from "./widgets";
|
|
14
|
-
export * from "./app-approval";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RAIL_PREFERENCES_MAX_BYTES, RailShortcutSchema } from "./rail-preferences";
|
|
3
|
+
import { AccessEntrySchema } from "./shared";
|
|
4
|
+
|
|
5
|
+
export const RailAccessSchema = AccessEntrySchema.extend({
|
|
6
|
+
principal: z.discriminatedUnion("type", [
|
|
7
|
+
z.object({ type: z.literal("authenticated") }).strict(),
|
|
8
|
+
z.object({ type: z.literal("user"), userId: z.uuid() }).strict(),
|
|
9
|
+
z.object({ type: z.literal("group"), groupId: z.uuid() }).strict(),
|
|
10
|
+
]),
|
|
11
|
+
permission: z.literal("read"),
|
|
12
|
+
});
|
|
13
|
+
export const RailAdminEntrySchema = z.object({ shortcut: RailShortcutSchema, access: z.array(RailAccessSchema) }).strict();
|
|
14
|
+
const revision = z.number().int().nonnegative().max(2_147_483_647);
|
|
15
|
+
export const RailAdminSchema = z.object({ revision, entries: z.array(RailAdminEntrySchema) }).strict();
|
|
16
|
+
/** Strip presentation metadata from editor drafts before sending or persisting grants. */
|
|
17
|
+
export const RailAdminInputSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
revision,
|
|
20
|
+
entries: z.array(
|
|
21
|
+
z
|
|
22
|
+
.object({
|
|
23
|
+
shortcut: RailShortcutSchema,
|
|
24
|
+
access: z.array(RailAccessSchema.pick({ principal: true, permission: true })),
|
|
25
|
+
})
|
|
26
|
+
.strict(),
|
|
27
|
+
),
|
|
28
|
+
})
|
|
29
|
+
.strict()
|
|
30
|
+
.superRefine((value, ctx) => {
|
|
31
|
+
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > RAIL_PREFERENCES_MAX_BYTES)
|
|
32
|
+
ctx.addIssue({ code: "custom", message: "Global app bar configuration exceeds 16 KiB" });
|
|
33
|
+
if (new Set(value.entries.map((entry) => entry.shortcut.id)).size !== value.entries.length)
|
|
34
|
+
ctx.addIssue({ code: "custom", message: "Shortcut IDs must be unique" });
|
|
35
|
+
});
|
|
36
|
+
export type RailAdminInput = z.infer<typeof RailAdminInputSchema>;
|
|
37
|
+
export type RailAdminState = z.infer<typeof RailAdminSchema>;
|
|
38
|
+
export type RailAdminEntry = z.infer<typeof RailAdminEntrySchema>;
|
|
@@ -51,3 +51,19 @@ export const RailPreferencesSchema = z
|
|
|
51
51
|
});
|
|
52
52
|
export type RailPreferences = z.infer<typeof RailPreferencesSchema>;
|
|
53
53
|
export const defaultRailPreferences = (): RailPreferences => ({ revision: 0, visibility: {}, shortcuts: [] });
|
|
54
|
+
|
|
55
|
+
/** Managed entries are separate from the writable personal API contract. */
|
|
56
|
+
export type RailSnapshot = RailPreferences & { managedShortcuts?: RailShortcut[] };
|
|
57
|
+
export const RailSnapshotSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
revision: z.number(),
|
|
60
|
+
visibility: z.record(z.string(), z.boolean()),
|
|
61
|
+
shortcuts: z.array(RailShortcutSchema),
|
|
62
|
+
managedShortcuts: z.array(RailShortcutSchema).optional(),
|
|
63
|
+
})
|
|
64
|
+
.strict()
|
|
65
|
+
.superRefine(({ managedShortcuts, ...personal }, ctx) => {
|
|
66
|
+
if (!RailPreferencesSchema.safeParse(personal).success) ctx.addIssue({ code: "custom", message: "Invalid personal app bar settings" });
|
|
67
|
+
if (new TextEncoder().encode(JSON.stringify(managedShortcuts ?? [])).byteLength > RAIL_PREFERENCES_MAX_BYTES)
|
|
68
|
+
ctx.addIssue({ code: "custom", message: "Managed shortcuts exceed the 16 KiB page budget" });
|
|
69
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AppAdminNavigationGroup, AppAppearance, AppAppearanceColor, AppPresentationCatalog } from "./app";
|
|
1
|
+
import type { AppAdminNavigationGroup, AppAppearance, AppAppearanceColor, AppPresentationCatalog, AppSearchLink } from "./app";
|
|
3
2
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "./capabilities";
|
|
4
3
|
import type { Role } from "./shared";
|
|
5
4
|
import type { DashboardWidgetPresentation } from "./widgets";
|
|
@@ -36,31 +35,7 @@ export type CapabilityRegistryEntry = {
|
|
|
36
35
|
presentation?: CapabilityPresentationCatalog;
|
|
37
36
|
};
|
|
38
37
|
|
|
39
|
-
export type AppRegistryHelpSummary =
|
|
40
|
-
|
|
41
|
-
export type HelpRegistryDocument = {
|
|
42
|
-
id: string;
|
|
43
|
-
title: string;
|
|
44
|
-
icon?: string;
|
|
45
|
-
description?: string;
|
|
46
|
-
order: number;
|
|
47
|
-
markdown: string;
|
|
48
|
-
/** Precomputed by the app. Older registry entries may omit it during rolling upgrades. */
|
|
49
|
-
searchText?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Full Markdown lives separately so normal app discovery stays small. */
|
|
53
|
-
export type HelpRegistryEntry = {
|
|
54
|
-
appId: string;
|
|
55
|
-
appName: string;
|
|
56
|
-
appIcon: string;
|
|
57
|
-
manifestHash: string;
|
|
58
|
-
/** Base language that owns the complete logical article set. Absent on legacy registrations. */
|
|
59
|
-
baseLocale?: string;
|
|
60
|
-
/** Partial localized article variants stored in the same bounded logical registration. */
|
|
61
|
-
documentsByLocale?: Readonly<Record<string, readonly HelpRegistryDocument[]>>;
|
|
62
|
-
documents: readonly HelpRegistryDocument[];
|
|
63
|
-
};
|
|
38
|
+
export type AppRegistryHelpSummary = { manifestHash: string; pageBase: string; baseLocale: string };
|
|
64
39
|
|
|
65
40
|
export type AppRegistryLegalLink = {
|
|
66
41
|
label: string;
|
|
@@ -76,6 +51,8 @@ export type AppRegistryWidget = {
|
|
|
76
51
|
};
|
|
77
52
|
|
|
78
53
|
export type AppRuntimeMetadata = {
|
|
54
|
+
/** Cloud version of the running app. Missing on apps built before versions were baked in. */
|
|
55
|
+
version?: string;
|
|
79
56
|
release: string;
|
|
80
57
|
syncVersion: string;
|
|
81
58
|
};
|
|
@@ -102,6 +79,7 @@ export type AppRegistryEntry = {
|
|
|
102
79
|
capabilities?: AppRegistryCapabilitySummary;
|
|
103
80
|
help?: AppRegistryHelpSummary;
|
|
104
81
|
legalLinks?: AppRegistryLegalLink[];
|
|
82
|
+
searchLinks?: readonly AppSearchLink[];
|
|
105
83
|
widgets?: AppRegistryWidget[];
|
|
106
84
|
/** Setting keys declared by this app. Used by admin tooling to avoid treating live app-owned settings as legacy. */
|
|
107
85
|
settingKeys?: readonly string[];
|
|
@@ -43,7 +43,7 @@ type StringLikeKind = "string" | "text" | "email" | "url" | "secret" | "image" |
|
|
|
43
43
|
export type AppSettingDef =
|
|
44
44
|
| (CommonDef & { kind: StringLikeKind; default: string; placeholder?: string; templateVars?: readonly string[] })
|
|
45
45
|
| (CommonDef & { kind: "boolean"; default: boolean })
|
|
46
|
-
| (CommonDef & { kind: "number"; default: number; min?: number; max?: number; placeholder?: string })
|
|
46
|
+
| (CommonDef & { kind: "number"; default: number; min?: number; max?: number; integer?: boolean; placeholder?: string })
|
|
47
47
|
| (CommonDef & { kind: "enum"; default: string; options: ReadonlyArray<SettingOption> })
|
|
48
48
|
| (CommonDef & { kind: "string_list"; default: readonly string[]; placeholder?: string })
|
|
49
49
|
| (CommonDef & { kind: "number_list"; default: readonly number[]; placeholder?: string });
|
package/src/contracts/shared.ts
CHANGED
|
@@ -288,12 +288,25 @@ export const AccountActivityListResponseSchema = z.object({
|
|
|
288
288
|
});
|
|
289
289
|
export type AccountActivityListResponse = z.infer<typeof AccountActivityListResponseSchema>;
|
|
290
290
|
|
|
291
|
+
export const AuthenticatedPrincipalSchema = z.discriminatedUnion("type", [
|
|
292
|
+
z.object({
|
|
293
|
+
type: z.literal("user").describe("One user account."),
|
|
294
|
+
userId: z.uuid().describe("Exact user ID returned by the Accounts directory."),
|
|
295
|
+
}),
|
|
296
|
+
z.object({
|
|
297
|
+
type: z.literal("group").describe("Members of one group."),
|
|
298
|
+
groupId: z.uuid().describe("Exact group ID returned by the Accounts directory."),
|
|
299
|
+
}),
|
|
300
|
+
z.object({
|
|
301
|
+
type: z.literal("service_account").describe("One service account."),
|
|
302
|
+
serviceAccountId: z.uuid().describe("Exact service account ID returned by the Accounts directory."),
|
|
303
|
+
}),
|
|
304
|
+
z.object({ type: z.literal("authenticated").describe("Every authenticated identity; no directory ID is needed.") }),
|
|
305
|
+
]);
|
|
306
|
+
|
|
291
307
|
export const PrincipalSchema = z.discriminatedUnion("type", [
|
|
292
|
-
|
|
293
|
-
z.object({ type: z.literal("
|
|
294
|
-
z.object({ type: z.literal("service_account"), serviceAccountId: z.uuid() }),
|
|
295
|
-
z.object({ type: z.literal("authenticated") }),
|
|
296
|
-
z.object({ type: z.literal("public") }),
|
|
308
|
+
...AuthenticatedPrincipalSchema.options,
|
|
309
|
+
z.object({ type: z.literal("public").describe("Public access where supported by the resource; no directory ID is needed.") }),
|
|
297
310
|
]);
|
|
298
311
|
export type Principal = z.infer<typeof PrincipalSchema>;
|
|
299
312
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** One metric plus six server phases and bounded routing metadata. */
|
|
4
|
+
export const WEB_VITALS_MAX_BYTES = 4096;
|
|
5
|
+
export const WebVitalsReportSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
appId: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(64)
|
|
11
|
+
.regex(/^[a-z0-9-]+$/),
|
|
12
|
+
routeTemplate: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(200)
|
|
16
|
+
.regex(/^\/[a-zA-Z0-9_/:.*-]*$/),
|
|
17
|
+
id: z.string().min(1).max(100),
|
|
18
|
+
name: z.enum(["LCP", "INP", "CLS"]),
|
|
19
|
+
value: z.number().nonnegative(),
|
|
20
|
+
navigationType: z.string().max(32),
|
|
21
|
+
serverTiming: z.partialRecord(
|
|
22
|
+
z.enum(["auth", "settings", "runtime", "ssr_data", "ssr_finalize", "ssr_render"]),
|
|
23
|
+
z.number().nonnegative(),
|
|
24
|
+
),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
export type WebVitalsReport = z.infer<typeof WebVitalsReportSchema>;
|
package/src/desktop/index.ts
CHANGED
|
@@ -311,6 +311,7 @@ const getSql = (): DesktopSql => {
|
|
|
311
311
|
const { Database } = requireFn("bun:sqlite") as { Database: new (path: string) => any };
|
|
312
312
|
const { dirname, resolve } = requireFn("node:path") as typeof import("node:path");
|
|
313
313
|
const { mkdirSync } = requireFn("node:fs") as typeof import("node:fs");
|
|
314
|
+
// Declared in the platform registry; this browser-safe module must not import zod-backed config.
|
|
314
315
|
const dbPath = process.env.CLOUD_DESKTOP_SQLITE_PATH ?? resolve(process.cwd(), ".local", "desktop.sqlite");
|
|
315
316
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
316
317
|
const db = new Database(dbPath);
|
package/src/desktop/solid.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deserialize as deserializeProps, serialize as serializeProps } from "seroval";
|
|
2
2
|
import type { Component, JSX, ParentProps } from "solid-js";
|
|
3
|
-
import { children, createContext,
|
|
3
|
+
import { children, createContext, createMemo, createSignal, For, onCleanup, onMount, Show, useContext } from "solid-js";
|
|
4
4
|
import { Dynamic, Portal } from "solid-js/web";
|
|
5
5
|
import {
|
|
6
6
|
type DesktopEnvironment,
|
package/src/index.ts
CHANGED
|
@@ -10,21 +10,23 @@ export {
|
|
|
10
10
|
unbindProcessSync,
|
|
11
11
|
} from "./_internal/process-sync";
|
|
12
12
|
export { APP_READINESS_PATH } from "./_internal/readiness";
|
|
13
|
-
export type {
|
|
13
|
+
export type {
|
|
14
|
+
AppRegistryDetail,
|
|
15
|
+
AppRegistryIssue,
|
|
16
|
+
AppRegistrySnapshot,
|
|
17
|
+
CapabilityRegistryRecord,
|
|
18
|
+
DashboardWidget,
|
|
19
|
+
} from "./_internal/registry";
|
|
14
20
|
export {
|
|
15
21
|
APP_REGISTRY_CONFIG,
|
|
16
22
|
APP_REGISTRY_PREFIX,
|
|
17
23
|
APP_REGISTRY_TTL_MS,
|
|
18
|
-
CAPABILITY_REGISTRY_CONFIG,
|
|
19
|
-
HELP_REGISTRY_CONFIG,
|
|
20
24
|
appRegistry,
|
|
25
|
+
CAPABILITY_REGISTRY_CONFIG,
|
|
21
26
|
capabilityRegistry,
|
|
22
27
|
getApp,
|
|
23
|
-
getHelp,
|
|
24
|
-
helpRegistry,
|
|
25
28
|
listApps,
|
|
26
29
|
listAppsDetailed,
|
|
27
|
-
listHelp,
|
|
28
30
|
listLegalLinks,
|
|
29
31
|
listWidgets,
|
|
30
32
|
readAppRegistrySnapshot,
|
package/src/server/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { defineHelp, defineHelpCollection, type HelpCollection, type HelpDefinit
|
|
|
8
8
|
export { DEFAULT_LOCALE, getLocale, LOCALE_COOKIE, LOCALE_HEADER, locale, preferredLocale, resolveLocale } from "./locale";
|
|
9
9
|
export type {
|
|
10
10
|
AuthContext,
|
|
11
|
+
InvocationExpectation,
|
|
11
12
|
InvocationProvenance,
|
|
12
13
|
RateLimitConfig,
|
|
13
14
|
RateLimitRouteOverride,
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
rateLimit,
|
|
27
28
|
rejectReservedWorkloadCredential,
|
|
28
29
|
requestLogger,
|
|
30
|
+
requireInvocation,
|
|
29
31
|
requiresAdmin,
|
|
30
32
|
requiresAuth,
|
|
31
33
|
requiresIpa,
|
|
@@ -56,6 +58,7 @@ export type {
|
|
|
56
58
|
ServiceErrorCode,
|
|
57
59
|
} from "./services";
|
|
58
60
|
export {
|
|
61
|
+
accessRevision,
|
|
59
62
|
buildAccessPrincipalCondition,
|
|
60
63
|
buildAccessPrincipalTierConditions,
|
|
61
64
|
createAccess,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
|
+
import { measureServerPhase } from "../../_internal/server-timing";
|
|
3
4
|
import type { MessageResponse, Role, RoleOrSpecial, User, UserProfile, UserProvider } from "../../contracts/shared";
|
|
4
5
|
import { isAccountExpired } from "../../services/account-model";
|
|
5
6
|
import { isReservedWorkloadApiCredential } from "../../services/identity/workload-auth";
|
|
@@ -215,10 +216,10 @@ const loadAuthenticatedActor = (
|
|
|
215
216
|
c: Context<AuthContext>,
|
|
216
217
|
options: Pick<RoleOptions, "oauthAudience"> = {},
|
|
217
218
|
): Promise<AuthenticatedActorResult> => {
|
|
218
|
-
if (options.oauthAudience) return loadAuthenticatedActorUncached(c, options);
|
|
219
|
+
if (options.oauthAudience) return measureServerPhase(c, "auth", () => loadAuthenticatedActorUncached(c, options));
|
|
219
220
|
const existing = actorResolutionByRequest.get(c);
|
|
220
221
|
if (existing) return existing;
|
|
221
|
-
const pending = loadAuthenticatedActorUncached(c, options);
|
|
222
|
+
const pending = measureServerPhase(c, "auth", () => loadAuthenticatedActorUncached(c, options));
|
|
222
223
|
actorResolutionByRequest.set(c, pending);
|
|
223
224
|
return pending;
|
|
224
225
|
};
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* Templates only: the request path itself never leaves the app, so ids
|
|
17
17
|
* and query strings stay out of telemetry by construction.
|
|
18
18
|
*/
|
|
19
|
+
|
|
20
|
+
import type { Context } from "hono";
|
|
19
21
|
import { createMiddleware } from "hono/factory";
|
|
20
22
|
import { routePath } from "hono/route";
|
|
21
23
|
import { ROUTE_TEMPLATE_HEADER } from "../../services/gateway";
|
|
@@ -32,14 +34,18 @@ const MAX_TEMPLATE_LENGTH = 200;
|
|
|
32
34
|
*/
|
|
33
35
|
const stripParamConstraints = (template: string): string => template.replace(/\{[^}]*\}/g, "");
|
|
34
36
|
|
|
37
|
+
export const matchedRouteTemplate = (c: Context): string | null => {
|
|
38
|
+
const template = routePath(c);
|
|
39
|
+
return !template || template === UNMATCHED ? null : stripParamConstraints(template).slice(0, MAX_TEMPLATE_LENGTH);
|
|
40
|
+
};
|
|
41
|
+
|
|
35
42
|
export const routeTemplate = createMiddleware(async (c, next) => {
|
|
36
43
|
await next();
|
|
37
44
|
|
|
38
45
|
// Best-effort: telemetry enrichment must never break a response.
|
|
39
46
|
try {
|
|
40
|
-
const template =
|
|
41
|
-
if (
|
|
42
|
-
c.res.headers.set(ROUTE_TEMPLATE_HEADER, stripParamConstraints(template).slice(0, MAX_TEMPLATE_LENGTH));
|
|
47
|
+
const template = matchedRouteTemplate(c);
|
|
48
|
+
if (template) c.res.headers.set(ROUTE_TEMPLATE_HEADER, template);
|
|
43
49
|
} catch {
|
|
44
50
|
// Immutable response headers or an unrouted context — nothing to report.
|
|
45
51
|
}
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createMiddleware } from "hono/factory";
|
|
11
11
|
import { ensureRuntimeWatcher, getCurrentRuntime } from "../../_internal/runtime-watcher";
|
|
12
|
+
import { measureServerPhase } from "../../_internal/server-timing";
|
|
12
13
|
import { routeTemplate } from "./route-template";
|
|
13
14
|
|
|
14
15
|
export const runtime = () =>
|
|
15
16
|
createMiddleware(async (c, next) => {
|
|
16
|
-
await ensureRuntimeWatcher
|
|
17
|
+
await measureServerPhase(c, "runtime", ensureRuntimeWatcher);
|
|
17
18
|
(c as unknown as { set: (k: string, v: unknown) => void }).set("runtime", getCurrentRuntime());
|
|
18
19
|
// Piggybacks on the middleware every proxied app already installs, so
|
|
19
20
|
// gateway telemetry gets per-endpoint route templates without any app
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `.use()` path instead.
|
|
14
14
|
*/
|
|
15
15
|
import { createMiddleware } from "hono/factory";
|
|
16
|
+
import { measureServerPhase } from "../../_internal/server-timing";
|
|
16
17
|
import {
|
|
17
18
|
type ActiveAnnouncementsResponse,
|
|
18
19
|
type AnnouncementCookieState,
|
|
@@ -38,18 +39,23 @@ export const settings = (opts?: { skipPrefixes?: readonly string[] }) => {
|
|
|
38
39
|
return createMiddleware(async (c, next) => {
|
|
39
40
|
const path = c.req.path;
|
|
40
41
|
if (!skip.some((p) => path.startsWith(p))) {
|
|
41
|
-
(c as unknown as { set: (k: string, v: unknown) => void }).set("settings", await loadSnapshot
|
|
42
|
-
}
|
|
43
|
-
const cookieHeader = c.req.header("Cookie") ?? null;
|
|
44
|
-
if (shouldLoadAnnouncements(path, cookieHeader)) {
|
|
45
|
-
const cookieState = parseAnnouncementCookieHeader(cookieHeader);
|
|
46
|
-
try {
|
|
47
|
-
const active = await announcements.active.forState({ state: cookieState });
|
|
48
|
-
(c as unknown as { set: (k: string, v: unknown) => void }).set("announcements", { ...active, cookieState });
|
|
49
|
-
} catch (error) {
|
|
50
|
-
log.warn("Failed to preload announcements", { error: error instanceof Error ? error.message : String(error) });
|
|
51
|
-
}
|
|
42
|
+
(c as unknown as { set: (k: string, v: unknown) => void }).set("settings", await measureServerPhase(c, "settings", loadSnapshot));
|
|
52
43
|
}
|
|
53
44
|
await next();
|
|
54
45
|
});
|
|
55
46
|
};
|
|
47
|
+
|
|
48
|
+
/** Called by SSR finalization, after redirects and non-HTML responses are known. */
|
|
49
|
+
export const preloadLayoutAnnouncements = async (c: import("hono").Context): Promise<void> => {
|
|
50
|
+
const path = c.req.path;
|
|
51
|
+
const cookieHeader = c.req.header("Cookie") ?? null;
|
|
52
|
+
if (shouldLoadAnnouncements(path, cookieHeader)) {
|
|
53
|
+
const cookieState = parseAnnouncementCookieHeader(cookieHeader);
|
|
54
|
+
try {
|
|
55
|
+
const active = await announcements.active.forState({ state: cookieState });
|
|
56
|
+
(c as unknown as { set: (k: string, v: unknown) => void }).set("announcements", { ...active, cookieState });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
log.warn("Failed to preload announcements", { error: error instanceof Error ? error.message : String(error) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
package/src/server/ratelimit.ts
CHANGED
|
@@ -86,7 +86,7 @@ export class RateLimitError extends Error {
|
|
|
86
86
|
// ==========================
|
|
87
87
|
|
|
88
88
|
export const ratelimit = (config: RateLimiterConfig): RateLimiter => {
|
|
89
|
-
// windowSecs: 0 — e.g. Number(
|
|
89
|
+
// windowSecs: 0 — e.g. `Number()` of an unset variable — made
|
|
90
90
|
// both window keys Infinity and collapse to one, elapsedRatio NaN, and
|
|
91
91
|
// `EXPIRE key 0` delete the counter on every first increment, so the
|
|
92
92
|
// weighted count never exceeded the limit and the limiter allowed
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { PermissionLevel, Principal } from "./access";
|
|
3
|
+
|
|
4
|
+
/** Compare the grants that were reviewed with those held under a resource lock. */
|
|
5
|
+
export function accessRevision(entries: ReadonlyArray<{ id: string; principal: Principal; permission: PermissionLevel }>): string {
|
|
6
|
+
const state = entries
|
|
7
|
+
.map((entry) => [entry.id, entry.permission, Object.entries(entry.principal).sort(([left], [right]) => left.localeCompare(right))])
|
|
8
|
+
.sort((left, right) => String(left[0]).localeCompare(String(right[0])));
|
|
9
|
+
return createHash("sha256").update(JSON.stringify(state)).digest("hex");
|
|
10
|
+
}
|
|
@@ -568,7 +568,9 @@ export const listUsersWithAccess = async (params: {
|
|
|
568
568
|
* Resolve display names for access entries.
|
|
569
569
|
* Populates the displayName field based on principal type.
|
|
570
570
|
*/
|
|
571
|
-
export const resolveDisplayNames = async
|
|
571
|
+
export const resolveDisplayNames = async <T extends { principal: Principal }>(
|
|
572
|
+
entries: T[],
|
|
573
|
+
): Promise<(T & { displayName: string; avatarHash?: string | null })[]> => {
|
|
572
574
|
const userIds = entries.filter((e) => e.principal.type === "user").map((e) => (e.principal as { type: "user"; userId: string }).userId);
|
|
573
575
|
|
|
574
576
|
const groupIds = entries
|
|
@@ -46,3 +46,4 @@ export { crypto, err, fail, isServiceError, ok, okMany, paginate, password, svg,
|
|
|
46
46
|
// Compat aliases for old API names
|
|
47
47
|
export const images = { generateFallback: _svg.generateAvatar, parseWebpDataUrl: _svg.parseWebpDataUrl };
|
|
48
48
|
export const generatePassword = _password.random;
|
|
49
|
+
export { accessRevision } from "./access-revision";
|
|
@@ -20,6 +20,15 @@ const readValue = async (key: string, fallback: boolean | string, db: typeof sql
|
|
|
20
20
|
return value;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
/** Decode a policy selected alongside a user, without a second database read. */
|
|
24
|
+
export const decodeAccountCategoryEnabled = async (encrypted: unknown): Promise<boolean> => {
|
|
25
|
+
if (encrypted === null || encrypted === undefined) return true;
|
|
26
|
+
if (typeof encrypted !== "string") throw new Error("Invalid account category configuration");
|
|
27
|
+
const value = await decryptValue(encrypted);
|
|
28
|
+
if (typeof value !== "boolean") throw new Error("Invalid account category configuration");
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
export const isAccountCategoryAllowed = async (
|
|
24
33
|
user: { provider: "local" | "ipa"; profile: "guest" | "user" },
|
|
25
34
|
db: typeof sql = sql,
|
|
@@ -37,6 +37,7 @@ import { listDuplicateEmails } from "./duplicate-emails";
|
|
|
37
37
|
import * as entities from "./entities";
|
|
38
38
|
import * as groups from "./groups";
|
|
39
39
|
import type { AccountsNotificationSender } from "./notification-sender";
|
|
40
|
+
import { PosixError } from "./posix";
|
|
40
41
|
import { accountRequestsEnabled } from "./request-policy";
|
|
41
42
|
import * as users from "./users";
|
|
42
43
|
|
|
@@ -141,6 +142,15 @@ const fromMutationResult = <T>(result: MutationResult<T>): Result<T> => {
|
|
|
141
142
|
return fail(toServiceError(result.status, result.error));
|
|
142
143
|
};
|
|
143
144
|
|
|
145
|
+
const fromGroupMutation = async <T>(operation: () => Promise<MutationResult<T>>): Promise<Result<T>> => {
|
|
146
|
+
try {
|
|
147
|
+
return fromMutationResult(await operation());
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof PosixError) return fail({ code: error.code, message: error.message, status: error.status });
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
144
154
|
const mapAccountRequestRow = (row: DbRow): AccountRequest => ({
|
|
145
155
|
id: row.id as string,
|
|
146
156
|
userId: row.user_id as string | null,
|
|
@@ -1119,7 +1129,7 @@ export const accountsAppService = {
|
|
|
1119
1129
|
target: { type: "group", label: config.name, provider: config.provider },
|
|
1120
1130
|
});
|
|
1121
1131
|
if (adminError) return adminError;
|
|
1122
|
-
const result =
|
|
1132
|
+
const result = await fromGroupMutation(() => groups.create(config));
|
|
1123
1133
|
return recordCompletedMutation({
|
|
1124
1134
|
action: "accounts.group.create",
|
|
1125
1135
|
actor: auditActor(config.actor),
|
|
@@ -1164,7 +1174,7 @@ export const accountsAppService = {
|
|
|
1164
1174
|
target,
|
|
1165
1175
|
});
|
|
1166
1176
|
if (adminError) return adminError;
|
|
1167
|
-
const result =
|
|
1177
|
+
const result = await fromGroupMutation(() => groups.makePosix(config));
|
|
1168
1178
|
return recordCompletedMutation({
|
|
1169
1179
|
action: "accounts.group.make_posix",
|
|
1170
1180
|
actor: auditActor(config.actor),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { sql } from "bun";
|
|
2
|
+
|
|
3
|
+
export const normalizeAccountEmail = (email: string): string => email.trim().toLowerCase();
|
|
4
|
+
|
|
5
|
+
/** Hold through the write (including a FreeIPA mutation), on the same transaction. */
|
|
6
|
+
export const lockAccountEmail = async (db: typeof sql, email: string): Promise<void> => {
|
|
7
|
+
// Take the table's ordinary write lock before the advisory lock. Local POSIX
|
|
8
|
+
// provisioning takes a stronger table lock first; this keeps their order compatible.
|
|
9
|
+
await db`LOCK TABLE auth.users IN ROW EXCLUSIVE MODE`;
|
|
10
|
+
await db`SELECT pg_advisory_xact_lock(hashtextextended('cloud:account-email:' || ${normalizeAccountEmail(email)}, 0))`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const findAccountsByEmail = (db: typeof sql, email: string) =>
|
|
14
|
+
db<
|
|
15
|
+
{
|
|
16
|
+
id: string;
|
|
17
|
+
uid: string;
|
|
18
|
+
provider: "local" | "ipa";
|
|
19
|
+
mail: string;
|
|
20
|
+
}[]
|
|
21
|
+
>`SELECT id, uid, provider, mail FROM auth.users WHERE lower(btrim(mail)) = ${normalizeAccountEmail(email)}`;
|
|
22
|
+
|
|
23
|
+
export const emailAlreadyUsed = () => ({ ok: false as const, error: "An account with this email already exists.", status: 409 as const });
|
|
@@ -4,9 +4,11 @@ import { freeipa } from "../../server/services";
|
|
|
4
4
|
import { getServiceIpaSession } from "../ipa/service-account";
|
|
5
5
|
import { toPgUuidArray } from "../postgres";
|
|
6
6
|
import { providers } from "../providers";
|
|
7
|
+
import type { AccountsActor } from "./authz";
|
|
7
8
|
import { buildBaseGroup } from "./base-group";
|
|
8
9
|
import { buildManagedGroupScopeCondition, buildMemberGroupScopeCondition } from "./group-sql";
|
|
9
10
|
import * as localGroups from "./local-groups";
|
|
11
|
+
import { posix } from "./posix";
|
|
10
12
|
|
|
11
13
|
type DbRow = Record<string, unknown>;
|
|
12
14
|
|
|
@@ -146,13 +148,14 @@ export const getManagedGroups = async (params: { id: string; provider?: UserProv
|
|
|
146
148
|
};
|
|
147
149
|
|
|
148
150
|
export const create = async (params: {
|
|
151
|
+
actor: AccountsActor;
|
|
149
152
|
provider: UserProvider;
|
|
150
153
|
name: string;
|
|
151
154
|
description?: string;
|
|
152
155
|
posix?: boolean;
|
|
153
156
|
}): Promise<MutationResult<BaseGroup>> => {
|
|
154
157
|
if (params.provider === "local") {
|
|
155
|
-
if (params.posix) return { ok:
|
|
158
|
+
if (params.posix) return { ok: true, data: await posix.createGroup({ id: params.actor.userId, roles: params.actor.roles }, params) };
|
|
156
159
|
return localGroups.create({ name: params.name, description: params.description });
|
|
157
160
|
}
|
|
158
161
|
const serviceSession = await getServiceIpaSession();
|
|
@@ -188,9 +191,17 @@ export const remove = async (params: { id: string; provider?: UserProvider }): P
|
|
|
188
191
|
});
|
|
189
192
|
};
|
|
190
193
|
|
|
191
|
-
export const makePosix = async (params: {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
export const makePosix = async (params: {
|
|
195
|
+
actor: AccountsActor;
|
|
196
|
+
id: string;
|
|
197
|
+
provider?: UserProvider;
|
|
198
|
+
}): Promise<MutationResult<{ gidnumber: number | null }>> => {
|
|
199
|
+
const provider = (await getGroup(params.id))?.provider;
|
|
200
|
+
if (!provider) return { ok: false, error: "Group not found", status: 404 };
|
|
201
|
+
if (provider === "local") {
|
|
202
|
+
const result = await posix.provisionGroup({ id: params.actor.userId, roles: params.actor.roles }, params.id);
|
|
203
|
+
return { ok: true, data: { gidnumber: result.gidNumber } };
|
|
204
|
+
}
|
|
194
205
|
const serviceSession = await getServiceIpaSession();
|
|
195
206
|
if (!serviceSession.ok) return serviceSession;
|
|
196
207
|
return providers.ipa.groups.makePosix({
|