@k2b/cloud 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { Button, bottomSheetAdaptiveOptions, dialogCore, IconButton, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, createUniqueId, For, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import type { AppAccessRule } from "../contracts/app-access";
|
|
4
|
+
import type { AccessEntry } from "../contracts/shared";
|
|
5
|
+
import type { AppAccessChange, AppAccessSnapshot } from "../services/app-access-management";
|
|
6
|
+
import { AppAccessClientError, appAccessAdapter } from "./app-access-adapter";
|
|
7
|
+
import { appAccessMessages } from "./app-access-messages";
|
|
8
|
+
import PermissionEditor, { type GrantableLevel } from "./PermissionEditor";
|
|
9
|
+
import { principalKey } from "./PrincipalPicker";
|
|
10
|
+
import { principalLabel } from "./principal-label";
|
|
11
|
+
|
|
12
|
+
export type AppAccessEditorProps = {
|
|
13
|
+
app: { id: string; name: string; access?: AppAccessRule };
|
|
14
|
+
initial?: AppAccessSnapshot;
|
|
15
|
+
repair?: boolean;
|
|
16
|
+
/** Return to the current page so its normal authorization renders the new state. */
|
|
17
|
+
onAccessLost?: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Application authors place this editor in their settings; Core uses it unchanged for explicit repair. */
|
|
21
|
+
export default function AppAccessEditor(props: AppAccessEditorProps) {
|
|
22
|
+
const locale = useLocale(),
|
|
23
|
+
t = () => appAccessMessages.resolve([locale()]).t;
|
|
24
|
+
const descriptionId = createUniqueId();
|
|
25
|
+
const adapter = appAccessAdapter(props.app.id, { repair: props.repair });
|
|
26
|
+
const [snapshot, setSnapshot] = createSignal(props.initial);
|
|
27
|
+
const [loading, setLoading] = createSignal(!props.initial);
|
|
28
|
+
// A problem's optional lead sentence is set in bold, as in the approved notices.
|
|
29
|
+
const [error, setError] = createSignal<{ lead?: string; text: string }>();
|
|
30
|
+
const [blocked, setBlocked] = createSignal(false);
|
|
31
|
+
const [forbidden, setForbidden] = createSignal(false);
|
|
32
|
+
const [showInherited, setShowInherited] = createSignal(true);
|
|
33
|
+
let section: HTMLElement | undefined;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
onCleanup(() => controller.abort());
|
|
36
|
+
/** `revoked` is false when only managing ended and use is confirmed, so the reloaded page shows no removal notice. */
|
|
37
|
+
const lost = (revoked = true) => {
|
|
38
|
+
if (!controller.signal.aborted)
|
|
39
|
+
(
|
|
40
|
+
props.onAccessLost ??
|
|
41
|
+
(() => {
|
|
42
|
+
const url = new URL(window.location.href);
|
|
43
|
+
if (revoked) url.searchParams.set("_cloud_access", "revoked");
|
|
44
|
+
else url.searchParams.delete("_cloud_access");
|
|
45
|
+
window.location.replace(url.href);
|
|
46
|
+
})
|
|
47
|
+
)();
|
|
48
|
+
};
|
|
49
|
+
const load = async (trigger?: HTMLButtonElement) => {
|
|
50
|
+
const hadFocus = trigger !== undefined && document.activeElement === trigger;
|
|
51
|
+
setLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
const fresh = await adapter.read(controller.signal);
|
|
54
|
+
const restoreFocus = hadFocus && (document.activeElement === trigger || document.activeElement === document.body);
|
|
55
|
+
setSnapshot(fresh);
|
|
56
|
+
setError(undefined);
|
|
57
|
+
setBlocked(false);
|
|
58
|
+
if (restoreFocus)
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
if (!controller.signal.aborted && document.activeElement === document.body)
|
|
61
|
+
(
|
|
62
|
+
section?.querySelector<HTMLElement>('input[role="combobox"]') ?? section?.querySelector<HTMLElement>("button:not(:disabled)")
|
|
63
|
+
)?.focus();
|
|
64
|
+
});
|
|
65
|
+
} catch (failure) {
|
|
66
|
+
if (!controller.signal.aborted) {
|
|
67
|
+
if (failure instanceof AppAccessClientError && failure.status === 403) {
|
|
68
|
+
setForbidden(true);
|
|
69
|
+
setSnapshot(undefined);
|
|
70
|
+
setError({ text: t().forbidden });
|
|
71
|
+
} else setError({ lead: t().loadFailedLead, text: t().loadFailed });
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
onMount(() => {
|
|
78
|
+
if (!props.initial && props.app.access?.kind === "grants") void load();
|
|
79
|
+
});
|
|
80
|
+
const errorText = (error: AppAccessClientError, change: AppAccessChange) =>
|
|
81
|
+
error.code === "LAST_MANAGER"
|
|
82
|
+
? t().lastManager
|
|
83
|
+
: error.status === 403
|
|
84
|
+
? t().forbidden
|
|
85
|
+
: error.code === "CONFLICT" && change.action === "grant"
|
|
86
|
+
? t().alreadyGranted
|
|
87
|
+
: error.code === "CONFLICT" || error.code === "NOT_FOUND"
|
|
88
|
+
? t().listChanged
|
|
89
|
+
: t().saveFailed;
|
|
90
|
+
|
|
91
|
+
const perform = async (change: AppAccessChange) => {
|
|
92
|
+
if (blocked()) throw new Error(t().uncertain);
|
|
93
|
+
let result: { entry?: AccessEntry; canUse: boolean };
|
|
94
|
+
try {
|
|
95
|
+
result = await adapter.change(change, controller.signal);
|
|
96
|
+
} catch (failure) {
|
|
97
|
+
if (failure instanceof AppAccessClientError && failure.status < 500) {
|
|
98
|
+
if (failure.status === 403) {
|
|
99
|
+
setForbidden(true);
|
|
100
|
+
setSnapshot(undefined);
|
|
101
|
+
setError({ text: t().forbidden });
|
|
102
|
+
} else if (failure.status === 400 || failure.status === 404 || failure.status === 409) {
|
|
103
|
+
// The same request cannot succeed again: offer the canonical list instead of a retry.
|
|
104
|
+
try {
|
|
105
|
+
setSnapshot(await adapter.read(controller.signal));
|
|
106
|
+
setError({ text: errorText(failure, change) });
|
|
107
|
+
} catch (readFailure) {
|
|
108
|
+
if (readFailure instanceof AppAccessClientError && readFailure.status === 403) {
|
|
109
|
+
setSnapshot(undefined);
|
|
110
|
+
setForbidden(true);
|
|
111
|
+
lost();
|
|
112
|
+
}
|
|
113
|
+
setError({ text: t().uncertain });
|
|
114
|
+
}
|
|
115
|
+
setBlocked(true);
|
|
116
|
+
}
|
|
117
|
+
throw new Error(errorText(failure, change));
|
|
118
|
+
}
|
|
119
|
+
// A lost response can follow a commit. Read the canonical list before offering another write.
|
|
120
|
+
let fresh: AppAccessSnapshot;
|
|
121
|
+
try {
|
|
122
|
+
fresh = await adapter.read(controller.signal);
|
|
123
|
+
} catch (readFailure) {
|
|
124
|
+
if (readFailure instanceof AppAccessClientError && readFailure.status === 403) {
|
|
125
|
+
setSnapshot(undefined);
|
|
126
|
+
setForbidden(true);
|
|
127
|
+
setError({ text: t().forbidden });
|
|
128
|
+
lost();
|
|
129
|
+
throw new Error(t().forbidden);
|
|
130
|
+
}
|
|
131
|
+
setBlocked(true);
|
|
132
|
+
setError({ text: t().uncertain });
|
|
133
|
+
throw new Error(t().uncertain);
|
|
134
|
+
}
|
|
135
|
+
setSnapshot(fresh);
|
|
136
|
+
const entry =
|
|
137
|
+
change.action === "grant"
|
|
138
|
+
? fresh.entries.find((entry) => principalKey(entry.principal) === principalKey(change.principal))
|
|
139
|
+
: fresh.entries.find((entry) => entry.id === change.accessId);
|
|
140
|
+
const applied = change.action === "revoke" ? !entry : entry?.permission === change.permission;
|
|
141
|
+
if (!applied) throw new Error(t().saveFailed);
|
|
142
|
+
result = { entry, canUse: fresh.canUse };
|
|
143
|
+
}
|
|
144
|
+
if (!result.canUse && !props.repair) lost();
|
|
145
|
+
else {
|
|
146
|
+
try {
|
|
147
|
+
setSnapshot(await adapter.read(controller.signal));
|
|
148
|
+
} catch (failure) {
|
|
149
|
+
// Use is confirmed here; only managing this list ended.
|
|
150
|
+
if (failure instanceof AppAccessClientError && failure.status === 403) lost(false);
|
|
151
|
+
else {
|
|
152
|
+
setBlocked(true);
|
|
153
|
+
setError({ text: t().savedRefreshFailed });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
};
|
|
159
|
+
const beforeChange = async (entry: AccessEntry, next: GrantableLevel | null) => {
|
|
160
|
+
if (props.repair || !snapshot()?.ownEntryIds.includes(entry.id) || (next !== null && entry.permission !== "admin")) return true;
|
|
161
|
+
const own =
|
|
162
|
+
snapshot()?.entries.filter((candidate) => candidate.id !== entry.id && snapshot()?.ownEntryIds.includes(candidate.id)) ?? [];
|
|
163
|
+
const losesUse = next === null && own.length === 0;
|
|
164
|
+
const losesManagement = !own.some((candidate) => candidate.permission === "admin");
|
|
165
|
+
// Name only managers the platform counts, so an empty group or an expired account is never offered as the rescue.
|
|
166
|
+
const others = (snapshot()?.entries ?? [])
|
|
167
|
+
.filter((candidate) => candidate.id !== entry.id && (candidate.effectiveManager ?? candidate.permission === "admin"))
|
|
168
|
+
.map((candidate) => principalLabel(candidate, locale()));
|
|
169
|
+
const remaining = t().remainingManagers({
|
|
170
|
+
managers: new Intl.ListFormat(locale(), { type: "conjunction" }).format(others),
|
|
171
|
+
count: others.length,
|
|
172
|
+
});
|
|
173
|
+
return (
|
|
174
|
+
(await dialogCore.open<boolean>(
|
|
175
|
+
(close) => (
|
|
176
|
+
<div class="mx-auto w-full max-w-[440px] p-5 text-primary max-md:pb-[calc(2rem+env(safe-area-inset-bottom,0px))]">
|
|
177
|
+
<h2 class="mb-2 text-[17px] font-semibold">
|
|
178
|
+
{entry.principal.type === "authenticated"
|
|
179
|
+
? t().removeAudienceTitle
|
|
180
|
+
: next === null
|
|
181
|
+
? t().removeSelfTitle
|
|
182
|
+
: t().changeSelfTitle}
|
|
183
|
+
</h2>
|
|
184
|
+
<p id={descriptionId} class="mb-4 text-[15px] text-secondary">
|
|
185
|
+
{(entry.principal.type === "authenticated"
|
|
186
|
+
? t().removeAudience
|
|
187
|
+
: losesUse
|
|
188
|
+
? t().removeSelf
|
|
189
|
+
: losesManagement
|
|
190
|
+
? t().removeSelfManagement
|
|
191
|
+
: next === null
|
|
192
|
+
? t().removeSelfEntry
|
|
193
|
+
: t().changeSelfEntry)({ name: props.app.name })}{" "}
|
|
194
|
+
{remaining}
|
|
195
|
+
</p>
|
|
196
|
+
<div class="flex justify-end gap-2">
|
|
197
|
+
<Button variant="secondary" style={{ "min-height": "36px", "font-size": "15px" }} onClick={() => close(false)}>
|
|
198
|
+
{t().cancel}
|
|
199
|
+
</Button>
|
|
200
|
+
<Button variant="danger" style={{ "min-height": "36px", "font-size": "15px" }} onClick={() => close(true)}>
|
|
201
|
+
{next === null ? t().remove : t().change}
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
),
|
|
206
|
+
{
|
|
207
|
+
...bottomSheetAdaptiveOptions,
|
|
208
|
+
role: "alertdialog",
|
|
209
|
+
ariaDescribedBy: descriptionId,
|
|
210
|
+
panelClassName: `${bottomSheetAdaptiveOptions.panelClassName} md:!w-[440px] !border-0`,
|
|
211
|
+
// Without ariaLabel, the dialog is labelled by its heading.
|
|
212
|
+
signal: controller.signal,
|
|
213
|
+
history: true,
|
|
214
|
+
},
|
|
215
|
+
)) === true
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
const fixedDescription = () => {
|
|
219
|
+
switch (props.app.access?.kind) {
|
|
220
|
+
case "resources":
|
|
221
|
+
return t().resources;
|
|
222
|
+
case "signedIn":
|
|
223
|
+
return t().fixedSignedIn;
|
|
224
|
+
case "noGuests":
|
|
225
|
+
return t().fixedNoGuests;
|
|
226
|
+
case "roles":
|
|
227
|
+
return props.app.access.roles.length === 1 && props.app.access.roles[0] === "admin" ? t().fixedAdmins : t().fixedRoles;
|
|
228
|
+
default:
|
|
229
|
+
return t().legacy;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
return (
|
|
233
|
+
<section ref={section} class="w-full max-w-[720px]" aria-label={t().access}>
|
|
234
|
+
<h2 class="mb-1 text-[15px] font-semibold">{t().whoUses({ name: props.app.name })}</h2>
|
|
235
|
+
<Show
|
|
236
|
+
when={props.app.access?.kind === "grants"}
|
|
237
|
+
fallback={<p class="max-w-[60ch] text-[15px] text-secondary">{fixedDescription()}</p>}
|
|
238
|
+
>
|
|
239
|
+
<p class="mb-4 max-w-[60ch] text-[15px] text-secondary">{t().description({ name: props.app.name })}</p>
|
|
240
|
+
<div class="flex flex-col gap-4">
|
|
241
|
+
<Show when={props.repair}>
|
|
242
|
+
<NoticeCard tone="info" icon="ti ti-shield-check">
|
|
243
|
+
<p>{t().repairNotice({ name: props.app.name })}</p>
|
|
244
|
+
</NoticeCard>
|
|
245
|
+
</Show>
|
|
246
|
+
<Show when={props.repair && snapshot() && !snapshot()!.hasEffectiveManager}>
|
|
247
|
+
<NoticeCard tone="warning" icon="ti ti-alert-triangle">
|
|
248
|
+
<p>
|
|
249
|
+
<strong class="font-semibold">{t().noManagerLead}</strong> {t().noManager}
|
|
250
|
+
</p>
|
|
251
|
+
</NoticeCard>
|
|
252
|
+
</Show>
|
|
253
|
+
{/* Repair already explains itself; the transition note is for the app's own managers. */}
|
|
254
|
+
<Show when={!props.repair && showInherited() && snapshot()?.inheritedSignedIn}>
|
|
255
|
+
<NoticeCard tone="info" icon="ti ti-info-circle">
|
|
256
|
+
<div class="flex items-start gap-2">
|
|
257
|
+
<p class="flex-1">{t().inherited({ name: props.app.name })}</p>
|
|
258
|
+
<IconButton
|
|
259
|
+
variant="ghost"
|
|
260
|
+
size="sm"
|
|
261
|
+
class="!text-inherit"
|
|
262
|
+
onClick={() => {
|
|
263
|
+
setShowInherited(false);
|
|
264
|
+
// The button leaves with its notice; the search is where editing continues.
|
|
265
|
+
queueMicrotask(() => section?.querySelector<HTMLElement>('input[role="combobox"]')?.focus());
|
|
266
|
+
}}
|
|
267
|
+
label={t().closeNotice}
|
|
268
|
+
>
|
|
269
|
+
<i class="ti ti-x text-[18px]" />
|
|
270
|
+
</IconButton>
|
|
271
|
+
</div>
|
|
272
|
+
</NoticeCard>
|
|
273
|
+
</Show>
|
|
274
|
+
<Show when={error()}>
|
|
275
|
+
{(problem) => (
|
|
276
|
+
<NoticeCard tone="danger" icon="ti ti-alert-circle" role="alert">
|
|
277
|
+
<div class="flex flex-wrap items-start gap-2">
|
|
278
|
+
<p class="flex-1">
|
|
279
|
+
<Show when={problem().lead}>
|
|
280
|
+
{(lead) => (
|
|
281
|
+
<>
|
|
282
|
+
<strong class="font-semibold">{lead()}</strong>{" "}
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</Show>
|
|
286
|
+
{problem().text}
|
|
287
|
+
</p>
|
|
288
|
+
<Show when={!forbidden()}>
|
|
289
|
+
<Button
|
|
290
|
+
variant="secondary"
|
|
291
|
+
style={{ "min-height": "36px", "font-size": "15px" }}
|
|
292
|
+
loading={loading()}
|
|
293
|
+
onClick={(event) => void load(event.currentTarget)}
|
|
294
|
+
>
|
|
295
|
+
{t().reload}
|
|
296
|
+
</Button>
|
|
297
|
+
</Show>
|
|
298
|
+
</div>
|
|
299
|
+
</NoticeCard>
|
|
300
|
+
)}
|
|
301
|
+
</Show>
|
|
302
|
+
<Show
|
|
303
|
+
when={snapshot()}
|
|
304
|
+
fallback={
|
|
305
|
+
<Show when={loading()}>
|
|
306
|
+
<div aria-busy="true" class="flex flex-col gap-1">
|
|
307
|
+
<For each={[0, 1, 2, 3]}>
|
|
308
|
+
{() => (
|
|
309
|
+
// The same grid as a loaded row, so the list keeps its place when the entries arrive.
|
|
310
|
+
<div class="grid min-h-[52px] grid-cols-[32px_minmax(0,1fr)_36px] items-center gap-x-3 gap-y-1.5 py-2 sm:flex sm:px-2 sm:py-1.5">
|
|
311
|
+
<span class="h-8 w-8 animate-pulse rounded-full bg-[var(--k2b-surface-muted)]" />
|
|
312
|
+
<span class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
313
|
+
<span class="h-3.5 w-2/5 animate-pulse rounded bg-[var(--k2b-surface-muted)]" />
|
|
314
|
+
<span class="h-3 w-1/4 animate-pulse rounded bg-[var(--k2b-surface-muted)]" />
|
|
315
|
+
</span>
|
|
316
|
+
<span class="col-start-2 row-start-2 h-9 animate-pulse rounded-[var(--ui-radius-control)] bg-[var(--k2b-surface-muted)] sm:w-44 sm:flex-none" />
|
|
317
|
+
<span class="w-9 shrink-0" />
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</For>
|
|
321
|
+
</div>
|
|
322
|
+
</Show>
|
|
323
|
+
}
|
|
324
|
+
>
|
|
325
|
+
{(state) => (
|
|
326
|
+
<PermissionEditor
|
|
327
|
+
expanded
|
|
328
|
+
initialEntries={state().entries}
|
|
329
|
+
effectiveEntries={(entries) =>
|
|
330
|
+
entries.filter((entry) => state().entries.find((candidate) => candidate.id === entry.id)?.effectiveManager !== false)
|
|
331
|
+
}
|
|
332
|
+
// Everyone matches all signed-in people, so that row is nobody's own marker.
|
|
333
|
+
ownEntryIds={state().ownEntryIds.filter((id) =>
|
|
334
|
+
state().entries.some((entry) => entry.id === id && entry.principal.type !== "authenticated"),
|
|
335
|
+
)}
|
|
336
|
+
placeholder={t().add}
|
|
337
|
+
signedIn={{ label: t().signedIn, description: t().signedInDescription }}
|
|
338
|
+
entryDetail={(entry) => {
|
|
339
|
+
const count = state().entries.find((candidate) => candidate.id === entry.id)?.groupMemberCount;
|
|
340
|
+
return entry.principal.type === "group" && count !== undefined ? t().group({ count }) : undefined;
|
|
341
|
+
}}
|
|
342
|
+
canEdit={!blocked()}
|
|
343
|
+
allowPublic={false}
|
|
344
|
+
allowAuthenticated
|
|
345
|
+
allowServiceAccounts
|
|
346
|
+
beforeChange={beforeChange}
|
|
347
|
+
allowedLevels={(principal) => {
|
|
348
|
+
const use = { level: "read" as const, label: t().use };
|
|
349
|
+
if (principal.type === "authenticated") return [use];
|
|
350
|
+
const manage = { level: "admin" as const, label: t().manage };
|
|
351
|
+
// New entries start at the first level; without a manager, the first change must add one.
|
|
352
|
+
return state().hasEffectiveManager ? [use, manage] : [manage, use];
|
|
353
|
+
}}
|
|
354
|
+
grantAccess={async (principal, permission) => {
|
|
355
|
+
if (permission !== "read" && permission !== "admin") throw new Error("Unsupported app permission");
|
|
356
|
+
const result = await perform({ action: "grant", principal, permission });
|
|
357
|
+
if (!result.entry) throw new Error(t().uncertain);
|
|
358
|
+
return result.entry;
|
|
359
|
+
}}
|
|
360
|
+
updateAccess={async (accessId, permission) => {
|
|
361
|
+
if (permission !== "read" && permission !== "admin") throw new Error("Unsupported app permission");
|
|
362
|
+
await perform({ action: "update", accessId, permission });
|
|
363
|
+
}}
|
|
364
|
+
revokeAccess={async (accessId) => {
|
|
365
|
+
await perform({ action: "revoke", accessId });
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
</Show>
|
|
370
|
+
</div>
|
|
371
|
+
<dl class="mt-6 grid gap-x-4 gap-y-1 text-sm sm:grid-cols-[max-content_1fr]">
|
|
372
|
+
<dt class="font-medium">{t().use}</dt>
|
|
373
|
+
<dd class="mb-2 text-secondary sm:mb-0">{t().useDescription}</dd>
|
|
374
|
+
<dt class="font-medium">{t().manage}</dt>
|
|
375
|
+
<dd class="text-secondary">{t().manageDescription}</dd>
|
|
376
|
+
</dl>
|
|
377
|
+
</Show>
|
|
378
|
+
</section>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LocaleProvider } from "@k2b/ui";
|
|
1
|
+
import { LocaleProvider, prompts } from "@k2b/ui";
|
|
2
2
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
3
3
|
import { render } from "solid-js/web";
|
|
4
4
|
import type { AccessEntry } from "../contracts/shared";
|
|
@@ -6,6 +6,8 @@ import PermissionEditor from "./PermissionEditor";
|
|
|
6
6
|
|
|
7
7
|
const host = document.getElementById("root");
|
|
8
8
|
if (!host) throw new Error("Missing harness root");
|
|
9
|
+
const confirmation = new URLSearchParams(location.search).get("confirmation");
|
|
10
|
+
const nextEvent = (name: string) => new Promise<void>((resolve) => document.addEventListener(name, () => resolve(), { once: true }));
|
|
9
11
|
render(() => {
|
|
10
12
|
let saved: AccessEntry[] = ["Mara Lind", "Lea Sommer", "Jonas Weber"].map((displayName, index) => ({
|
|
11
13
|
id: `access-${index}`,
|
|
@@ -32,6 +34,21 @@ render(() => {
|
|
|
32
34
|
{(entries) => (
|
|
33
35
|
<PermissionEditor
|
|
34
36
|
initialEntries={entries()}
|
|
37
|
+
beforeChange={
|
|
38
|
+
confirmation
|
|
39
|
+
? async () => {
|
|
40
|
+
document.documentElement.dataset.confirming = "true";
|
|
41
|
+
if (confirmation === "delayed") await nextEvent("open-access-confirmation");
|
|
42
|
+
const confirmed = await prompts.confirm("Change this person's access?", { title: "Confirm access", history: true });
|
|
43
|
+
// Keep the caller pending until the test has observed the dialog's focus-return frame.
|
|
44
|
+
const settled = nextEvent("settle-access-confirmation");
|
|
45
|
+
document.documentElement.dataset.confirmationClosed = "true";
|
|
46
|
+
await settled;
|
|
47
|
+
delete document.documentElement.dataset.confirming;
|
|
48
|
+
return confirmed === true;
|
|
49
|
+
}
|
|
50
|
+
: undefined
|
|
51
|
+
}
|
|
35
52
|
// Equal-width labels isolate pending-state layout from a normal change in label width.
|
|
36
53
|
allowedLevels={[
|
|
37
54
|
{ level: "read", label: "Level 1" },
|
|
@@ -43,6 +60,7 @@ render(() => {
|
|
|
43
60
|
}}
|
|
44
61
|
updateAccess={(id, permission) =>
|
|
45
62
|
new Promise<void>((resolve) => {
|
|
63
|
+
document.documentElement.dataset.writes = String(Number(document.documentElement.dataset.writes ?? 0) + 1);
|
|
46
64
|
// Existing tests use this mark and event to control the save.
|
|
47
65
|
document.documentElement.dataset.saving = "true";
|
|
48
66
|
document.addEventListener(
|
|
@@ -56,7 +74,9 @@ render(() => {
|
|
|
56
74
|
);
|
|
57
75
|
})
|
|
58
76
|
}
|
|
59
|
-
revokeAccess={async () => {
|
|
77
|
+
revokeAccess={async () => {
|
|
78
|
+
document.documentElement.dataset.writes = String(Number(document.documentElement.dataset.writes ?? 0) + 1);
|
|
79
|
+
}}
|
|
60
80
|
/>
|
|
61
81
|
)}
|
|
62
82
|
</Show>
|