@k2b/cloud 0.43.0 → 0.45.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 +11 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- 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 +21 -44
- 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 +27 -22
- 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 +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- 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/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 +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +6 -1
- 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 +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -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/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- 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/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- 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/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- 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,101 @@
|
|
|
1
|
+
import { S3Client } from "bun";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readStates } from "../services/settings/store";
|
|
4
|
+
|
|
5
|
+
export const objectStoreSettingKeys = [
|
|
6
|
+
"storage.endpoint",
|
|
7
|
+
"storage.region",
|
|
8
|
+
"storage.bucket",
|
|
9
|
+
"storage.access_key_id",
|
|
10
|
+
"storage.secret_access_key",
|
|
11
|
+
"storage.max_object_bytes",
|
|
12
|
+
] as const;
|
|
13
|
+
const bucket = z
|
|
14
|
+
.string()
|
|
15
|
+
.min(3)
|
|
16
|
+
.max(63)
|
|
17
|
+
.regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/);
|
|
18
|
+
const Config = z.object({
|
|
19
|
+
endpoint: z.string().min(1),
|
|
20
|
+
region: z.string().min(1),
|
|
21
|
+
bucket,
|
|
22
|
+
access_key_id: z.string().min(1),
|
|
23
|
+
secret_access_key: z.string().min(1),
|
|
24
|
+
max_object_bytes: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(5 * 1024 ** 3),
|
|
29
|
+
});
|
|
30
|
+
export class ObjectStoreConfigurationError extends Error {}
|
|
31
|
+
export class ObjectStoreConnectionError extends Error {}
|
|
32
|
+
let cached: { signature: string; client: S3Client } | undefined;
|
|
33
|
+
|
|
34
|
+
export const objectStoreBackend = async () => {
|
|
35
|
+
// Read one coherent DB snapshot, including stored-state checks. No local TTL:
|
|
36
|
+
// changes and secret rotation apply to the next operation in every process.
|
|
37
|
+
const states = await readStates(objectStoreSettingKeys);
|
|
38
|
+
if (objectStoreSettingKeys.some((key) => states.get(key)?.stored === "unreadable")) {
|
|
39
|
+
throw new ObjectStoreConfigurationError(
|
|
40
|
+
"Storage settings cannot be decrypted. Check this process uses the installation APP_SECRET, then replace unreadable settings in Administration > Settings > Storage.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const parsed = Config.safeParse(
|
|
44
|
+
Object.fromEntries(objectStoreSettingKeys.map((key) => [key.slice("storage.".length), states.get(key)?.value])),
|
|
45
|
+
);
|
|
46
|
+
if (!parsed.success)
|
|
47
|
+
throw new ObjectStoreConfigurationError(
|
|
48
|
+
"Storage is not configured. Set the Garage endpoint, region, bucket and credentials in Administration > Settings > Storage.",
|
|
49
|
+
);
|
|
50
|
+
const config = parsed.data;
|
|
51
|
+
const endpoint = URL.parse(config.endpoint);
|
|
52
|
+
if (
|
|
53
|
+
!endpoint ||
|
|
54
|
+
!["http:", "https:"].includes(endpoint.protocol) ||
|
|
55
|
+
endpoint.username ||
|
|
56
|
+
endpoint.password ||
|
|
57
|
+
endpoint.search ||
|
|
58
|
+
endpoint.hash ||
|
|
59
|
+
endpoint.pathname !== "/"
|
|
60
|
+
) {
|
|
61
|
+
throw new ObjectStoreConfigurationError("Storage endpoint must be an HTTP(S) origin without credentials, path, query or fragment.");
|
|
62
|
+
}
|
|
63
|
+
const signature = JSON.stringify(config);
|
|
64
|
+
if (cached?.signature !== signature) {
|
|
65
|
+
const options = {
|
|
66
|
+
endpoint: endpoint.href,
|
|
67
|
+
accessKeyId: config.access_key_id,
|
|
68
|
+
secretAccessKey: config.secret_access_key,
|
|
69
|
+
region: config.region,
|
|
70
|
+
};
|
|
71
|
+
cached = { signature, client: new S3Client({ ...options, bucket: config.bucket }) };
|
|
72
|
+
}
|
|
73
|
+
return { client: cached.client, limit: config.max_object_bytes };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Internal administrator check; never an application API or a credential response. */
|
|
77
|
+
export const testObjectStoreConnection = async (): Promise<void> => {
|
|
78
|
+
const backend = await objectStoreBackend();
|
|
79
|
+
const file = backend.client.file(`cloud-connection-test/${crypto.randomUUID()}`);
|
|
80
|
+
const content = "Cloud storage connection test";
|
|
81
|
+
let step = "write";
|
|
82
|
+
try {
|
|
83
|
+
await file.write(content, { type: "text/plain" });
|
|
84
|
+
step = "read";
|
|
85
|
+
if ((await file.text()) !== content) throw new Error("Unexpected content");
|
|
86
|
+
step = "delete";
|
|
87
|
+
await file.delete();
|
|
88
|
+
if (await file.exists()) throw new Error("Object still exists");
|
|
89
|
+
} catch {
|
|
90
|
+
try {
|
|
91
|
+
await file.delete();
|
|
92
|
+
} catch {
|
|
93
|
+
throw new ObjectStoreConnectionError(
|
|
94
|
+
`Storage test failed at ${step}; cleanup failed. Remove cloud-connection-test/ objects after fixing the connection.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
throw new ObjectStoreConnectionError(
|
|
98
|
+
`Storage test failed at ${step}. Check the saved settings, Garage availability and bucket permissions, then retry.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// S3's complete object key, including the area, is limited to 1,024 UTF-8 bytes.
|
|
2
|
+
// Reject percent signs altogether, including nested encodings, before Bun sees a key.
|
|
3
|
+
export const validateObjectKey = (key: string, area: string, allowPrefix = false): string => {
|
|
4
|
+
if (typeof key !== "string" || !key.isWellFormed() || key !== key.normalize("NFC") || /[\\%\p{Cc}\p{Cf}]/u.test(key)) {
|
|
5
|
+
throw new TypeError("Object keys must be NFC text without controls, backslashes, or percent escapes.");
|
|
6
|
+
}
|
|
7
|
+
if (allowPrefix && key === "") return key;
|
|
8
|
+
const path = allowPrefix && key.endsWith("/") ? key.slice(0, -1) : key;
|
|
9
|
+
if (!path || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
10
|
+
throw new TypeError("Object keys must be relative paths without empty, dot, or parent segments.");
|
|
11
|
+
}
|
|
12
|
+
if (Buffer.byteLength(area + key) > 1024) throw new RangeError("Object key exceeds S3's 1,024-byte limit including its area.");
|
|
13
|
+
return key;
|
|
14
|
+
};
|
|
@@ -2,20 +2,25 @@ import type { PlatformPermission } from "../contracts/outgoing-mail";
|
|
|
2
2
|
|
|
3
3
|
/** Identity of the application started in this process, derived from its declaration. */
|
|
4
4
|
let applicationId: string | undefined;
|
|
5
|
+
let applicationPackage: string | undefined;
|
|
5
6
|
let platformPermissions: readonly PlatformPermission[] = [];
|
|
6
7
|
export const getProcessPlatformPermissions = (): readonly PlatformPermission[] => platformPermissions;
|
|
7
8
|
|
|
8
9
|
export const getProcessApplicationId = (): string | undefined => applicationId;
|
|
9
10
|
|
|
10
|
-
export const
|
|
11
|
-
|
|
11
|
+
export const getProcessApplicationPackage = (): string | undefined => applicationPackage;
|
|
12
|
+
|
|
13
|
+
export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = [], packageName = id): void => {
|
|
14
|
+
if (applicationId !== undefined && (applicationId !== id || applicationPackage !== packageName)) {
|
|
12
15
|
throw new Error(`This process already hosts application "${applicationId}"`);
|
|
13
16
|
}
|
|
14
17
|
applicationId = id;
|
|
18
|
+
applicationPackage = packageName;
|
|
15
19
|
platformPermissions = [...permissions];
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
export const clearProcessApplicationId = (): void => {
|
|
19
23
|
applicationId = undefined;
|
|
24
|
+
applicationPackage = undefined;
|
|
20
25
|
platformPermissions = [];
|
|
21
26
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLOUD_CLI_MODULE_NAME } from "../cli/plugin";
|
|
2
|
+
import { APP_ADMIN_AREAS } from "../contracts/app";
|
|
2
3
|
import { AppAccessRuleSchema } from "../contracts/app-access";
|
|
3
4
|
import { validateInstallable } from "../contracts/installable";
|
|
4
5
|
import { PlatformPermissionSchema } from "../contracts/outgoing-mail";
|
|
@@ -19,6 +20,9 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
19
20
|
for (const field of ["id", "name", "icon", "description", "baseUrl"] as const) {
|
|
20
21
|
if (!isString(value[field]) || value[field].length === 0) return invalid(field, "a non-empty string");
|
|
21
22
|
}
|
|
23
|
+
if (value.package !== undefined && (typeof value.package !== "string" || !/^[a-z][a-z0-9-]{0,79}$/.test(value.package))) {
|
|
24
|
+
return invalid("package", "a lowercase package name of 1–80 characters");
|
|
25
|
+
}
|
|
22
26
|
try {
|
|
23
27
|
const baseUrl = new URL(value.baseUrl as string);
|
|
24
28
|
if (!(["http:", "https:"] as const).includes(baseUrl.protocol as "http:" | "https:") || baseUrl.username || baseUrl.password) {
|
|
@@ -100,7 +104,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
100
104
|
(group) =>
|
|
101
105
|
!isRecord(group) ||
|
|
102
106
|
(group.id !== undefined && !isString(group.id)) ||
|
|
103
|
-
(group.section !== undefined && group.section
|
|
107
|
+
(group.section !== undefined && !(APP_ADMIN_AREAS as readonly unknown[]).includes(group.section)) ||
|
|
104
108
|
!isString(group.label) ||
|
|
105
109
|
!Array.isArray(group.links) ||
|
|
106
110
|
group.links.some((link) => !isRecord(link) || !isString(link.label) || !isString(link.href) || !isString(link.icon)),
|
|
@@ -141,6 +145,12 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
141
145
|
if (value.pwa.requiresRoles !== undefined && !isStringArray(value.pwa.requiresRoles)) {
|
|
142
146
|
return invalid("pwa.requiresRoles", "an array of strings");
|
|
143
147
|
}
|
|
148
|
+
for (const field of ["badge", "summary"] as const) {
|
|
149
|
+
const path = value.pwa[field];
|
|
150
|
+
if (path !== undefined && (!isString(path) || !isOwnAppPath(path, value.routes))) {
|
|
151
|
+
return invalid(`pwa.${field}`, "a same-origin path below one of the app's routes");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
144
154
|
}
|
|
145
155
|
if (value.widgets !== undefined) {
|
|
146
156
|
if (
|
|
@@ -5,6 +5,7 @@ import { calendarSourceIssues } from "../contracts/calendar-source";
|
|
|
5
5
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
|
|
6
6
|
import { fileProviderIssues } from "../contracts/file-provider";
|
|
7
7
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
8
|
+
import { resourcePageIssue } from "../contracts/resource-pages";
|
|
8
9
|
import { type Role, RoleSchema } from "../contracts/shared";
|
|
9
10
|
import {
|
|
10
11
|
type DashboardWidgetPresentation,
|
|
@@ -333,7 +334,13 @@ export const resolveLiveCapabilityRegistryEntry = (
|
|
|
333
334
|
if (!endpoint) return null;
|
|
334
335
|
try {
|
|
335
336
|
const read = parseCapabilityManifest(record.manifest, app.id);
|
|
336
|
-
const
|
|
337
|
+
const supported = withValidCalendarSource(withValidFileProvider(read));
|
|
338
|
+
const manifest = supported.resourcePages
|
|
339
|
+
? {
|
|
340
|
+
...supported,
|
|
341
|
+
resourcePages: supported.resourcePages.filter((page) => !resourcePageIssue(page.path, app.routes, app.id)),
|
|
342
|
+
}
|
|
343
|
+
: supported;
|
|
337
344
|
const presentation = compileCapabilityPresentation(manifest, record.presentation, "reader");
|
|
338
345
|
if (app.capabilities.protocolVersion !== manifest.protocolVersion || app.capabilities.manifestHash !== manifest.manifestHash) {
|
|
339
346
|
return null;
|
|
@@ -71,7 +71,14 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
71
71
|
),
|
|
72
72
|
legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
73
73
|
searchLinks: e.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
|
|
74
|
-
pwa: e.pwa
|
|
74
|
+
pwa: e.pwa
|
|
75
|
+
? {
|
|
76
|
+
href: e.pwa.href,
|
|
77
|
+
requiresRoles: e.pwa.requiresRoles ? ([...e.pwa.requiresRoles] as Role[]) : undefined,
|
|
78
|
+
...(e.pwa.badge ? { badge: e.pwa.badge } : {}),
|
|
79
|
+
...(e.pwa.summary ? { summary: e.pwa.summary } : {}),
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
75
82
|
installable: e.installable ? structuredClone(e.installable) : undefined,
|
|
76
83
|
openapi: e.openapi,
|
|
77
84
|
};
|
|
@@ -217,6 +217,8 @@ export default function AppAccessEditor(props: AppAccessEditorProps) {
|
|
|
217
217
|
};
|
|
218
218
|
const fixedDescription = () => {
|
|
219
219
|
switch (props.app.access?.kind) {
|
|
220
|
+
case "resources":
|
|
221
|
+
return t().resources;
|
|
220
222
|
case "signedIn":
|
|
221
223
|
return t().fixedSignedIn;
|
|
222
224
|
case "noGuests":
|
|
@@ -2,16 +2,18 @@ import { query } from "@k2b/stdlib/solid";
|
|
|
2
2
|
import { Button, InlineGuidance, useLocale } from "@k2b/ui";
|
|
3
3
|
import { createSignal, createUniqueId, For, Show } from "solid-js";
|
|
4
4
|
import { CloudAvatar } from "../account/Avatar";
|
|
5
|
-
import type {
|
|
5
|
+
import type { EntityListItem, PaginationResponse } from "../contracts/shared";
|
|
6
6
|
import { accessMessages } from "./messages";
|
|
7
|
+
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
7
8
|
|
|
8
9
|
/** Members per request. Further pages load only when the viewer asks for them. */
|
|
9
10
|
const PAGE_SIZE = 20;
|
|
11
|
+
type GroupMember = Exclude<EntityListItem, { kind: "group" }>;
|
|
10
12
|
|
|
11
13
|
type MembersPage =
|
|
12
14
|
/** The directory withholds group members from this viewer (guest accounts). */
|
|
13
15
|
| { visible: false; directoryGroup: boolean }
|
|
14
|
-
| { visible: true; directoryGroup: boolean; members:
|
|
16
|
+
| { visible: true; directoryGroup: boolean; members: GroupMember[]; total: number; page: number; hasNext: boolean };
|
|
15
17
|
|
|
16
18
|
/** Reads the Accounts directory with the viewer's own visibility: `null` when it is withheld. */
|
|
17
19
|
const listEntities = async (params: Record<string, string>, signal: AbortSignal) => {
|
|
@@ -38,7 +40,7 @@ export const createGroupCoverage = (groupId: string) => {
|
|
|
38
40
|
const page = cursor ?? 1;
|
|
39
41
|
const [list, group] = await Promise.all([
|
|
40
42
|
listEntities(
|
|
41
|
-
{ kinds: "user", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
|
|
43
|
+
{ kinds: "user,service_account", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
|
|
42
44
|
abortSignal,
|
|
43
45
|
),
|
|
44
46
|
// Only the first page needs the group itself, for the directory hint.
|
|
@@ -49,7 +51,7 @@ export const createGroupCoverage = (groupId: string) => {
|
|
|
49
51
|
return {
|
|
50
52
|
visible: true,
|
|
51
53
|
directoryGroup,
|
|
52
|
-
members: list.items.
|
|
54
|
+
members: list.items.filter((item) => item.kind !== "group"),
|
|
53
55
|
total: list.pagination.total,
|
|
54
56
|
page,
|
|
55
57
|
hasNext: list.pagination.has_next,
|
|
@@ -146,7 +148,20 @@ export function GroupCoveragePanel(props: { coverage: GroupCoverage; groupName:
|
|
|
146
148
|
<Show when={shown().length > 0}>
|
|
147
149
|
<ul ref={list} tabindex="-1" aria-label={t().groupMembersOf({ name: props.groupName })} class="flex flex-col gap-1 outline-none">
|
|
148
150
|
<For each={shown()}>
|
|
149
|
-
{(
|
|
151
|
+
{(member) => {
|
|
152
|
+
if (member.kind === "service_account") {
|
|
153
|
+
const kind = () => serviceAccountKindDisplay(member.serviceAccount.kind, t());
|
|
154
|
+
return (
|
|
155
|
+
<li class="flex min-w-0 items-center gap-2 text-sm">
|
|
156
|
+
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
157
|
+
<i class={`ti ${kind().icon}`} aria-hidden="true" />
|
|
158
|
+
</span>
|
|
159
|
+
<span class="truncate">{member.serviceAccount.name}</span>
|
|
160
|
+
<span class="shrink-0 text-xs text-muted">{kind().label}</span>
|
|
161
|
+
</li>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const user = member.user;
|
|
150
165
|
const name = user.displayName || user.mail || user.uid;
|
|
151
166
|
return (
|
|
152
167
|
<li class="flex min-w-0 items-center gap-2 text-sm">
|
|
@@ -3,7 +3,7 @@ import { announce, Button, IconButton, Placeholder, prompts, Select, SelectChip,
|
|
|
3
3
|
import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js";
|
|
4
4
|
import { createStore, reconcile } from "solid-js/store";
|
|
5
5
|
import { CloudAvatar } from "../account/Avatar";
|
|
6
|
-
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
|
|
6
|
+
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
|
|
7
7
|
import { createGroupCoverage, type GroupCoverage, GroupCoveragePanel, GroupCoverageToggle } from "./GroupCoverage";
|
|
8
8
|
import { isManagerEntry } from "./managers";
|
|
9
9
|
import { accessMessages } from "./messages";
|
|
@@ -81,9 +81,13 @@ type PermissionEditorProps = {
|
|
|
81
81
|
* true; resources with an explicit principal allowlist can disable it. */
|
|
82
82
|
allowAuthenticated?: boolean;
|
|
83
83
|
|
|
84
|
-
/** Allow granting service accounts.
|
|
85
|
-
*
|
|
84
|
+
/** Allow granting service accounts. Defaults to true, including agents.
|
|
85
|
+
* Disable only when the resource documents a user-backed identity requirement. */
|
|
86
86
|
allowServiceAccounts?: boolean;
|
|
87
|
+
/** Optional directory filters for the resource's accepted credential kinds. */
|
|
88
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
89
|
+
/** Grant selection defaults to active accounts; existing entries are unaffected. */
|
|
90
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
87
91
|
|
|
88
92
|
/** The principal search's placeholder, in the consumer's terms. Defaults to the principal kinds it offers. */
|
|
89
93
|
placeholder?: string;
|
|
@@ -445,6 +449,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
445
449
|
allowPublic={allowPublic()}
|
|
446
450
|
allowAuthenticated={allowAuthenticated()}
|
|
447
451
|
allowServiceAccounts={props.allowServiceAccounts}
|
|
452
|
+
serviceAccountKinds={props.serviceAccountKinds}
|
|
453
|
+
serviceAccountStatus={props.serviceAccountStatus ?? "active"}
|
|
448
454
|
placeholder={props.placeholder}
|
|
449
455
|
signedIn={props.signedIn}
|
|
450
456
|
disabled={saving()}
|
|
@@ -563,6 +569,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
563
569
|
allowPublic={allowPublic()}
|
|
564
570
|
allowAuthenticated={allowAuthenticated()}
|
|
565
571
|
allowServiceAccounts={props.allowServiceAccounts}
|
|
572
|
+
serviceAccountKinds={props.serviceAccountKinds}
|
|
573
|
+
serviceAccountStatus={props.serviceAccountStatus ?? "active"}
|
|
566
574
|
placeholder={props.placeholder}
|
|
567
575
|
signedIn={props.signedIn}
|
|
568
576
|
readOnly={grantPending()}
|
|
@@ -645,7 +653,7 @@ function AccessEntryRow(props: {
|
|
|
645
653
|
>
|
|
646
654
|
<Show when={!props.expanded} fallback={<PrincipalAvatar entry={props.entry} label={displayName()} />}>
|
|
647
655
|
<Show
|
|
648
|
-
when={props.entry.principal.type === "user"}
|
|
656
|
+
when={props.entry.principal.type === "user" || props.entry.principal.type === "service_account"}
|
|
649
657
|
fallback={
|
|
650
658
|
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
651
659
|
<i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
|
|
@@ -654,8 +662,15 @@ function AccessEntryRow(props: {
|
|
|
654
662
|
>
|
|
655
663
|
<CloudAvatar
|
|
656
664
|
username={displayName()}
|
|
657
|
-
userId={
|
|
665
|
+
userId={
|
|
666
|
+
props.entry.principal.type === "user"
|
|
667
|
+
? props.entry.principal.userId
|
|
668
|
+
: props.entry.principal.type === "service_account"
|
|
669
|
+
? props.entry.principal.serviceAccountId
|
|
670
|
+
: undefined
|
|
671
|
+
}
|
|
658
672
|
avatarHash={props.entry.avatarHash}
|
|
673
|
+
kind={props.entry.principal.type === "service_account" ? "service_account" : "user"}
|
|
659
674
|
size="xs"
|
|
660
675
|
class="h-7 w-7"
|
|
661
676
|
/>
|
|
@@ -2,9 +2,9 @@ import { Show } from "solid-js";
|
|
|
2
2
|
import { CloudAvatar } from "../account/Avatar";
|
|
3
3
|
import type { AccessEntry } from "../contracts/shared";
|
|
4
4
|
|
|
5
|
-
// One hue per principal kind from the avatar palette, so a group
|
|
6
|
-
//
|
|
7
|
-
const hues = { group: "#22c55e",
|
|
5
|
+
// One hue per principal kind from the avatar palette, so a group and all signed-in people read apart at a glance.
|
|
6
|
+
// People use a neutral background; a service account shows its robot or photo, never a bare icon.
|
|
7
|
+
const hues = { group: "#22c55e", authenticated: "#3b82f6" } as const;
|
|
8
8
|
|
|
9
9
|
/** The 32px avatar of an access entry in the expanded editor and on the denial page. */
|
|
10
10
|
export default function PrincipalAvatar(props: {
|
|
@@ -15,8 +15,6 @@ export default function PrincipalAvatar(props: {
|
|
|
15
15
|
switch (props.entry.principal.type) {
|
|
16
16
|
case "group":
|
|
17
17
|
return "ti-users-group";
|
|
18
|
-
case "service_account":
|
|
19
|
-
return "ti-robot";
|
|
20
18
|
case "authenticated":
|
|
21
19
|
return "ti-world";
|
|
22
20
|
default:
|
|
@@ -25,27 +23,38 @@ export default function PrincipalAvatar(props: {
|
|
|
25
23
|
};
|
|
26
24
|
const hue = () => {
|
|
27
25
|
const type = props.entry.principal.type;
|
|
28
|
-
return type === "group" || type === "
|
|
26
|
+
return type === "group" || type === "authenticated" ? hues[type] : undefined;
|
|
29
27
|
};
|
|
30
28
|
return (
|
|
31
29
|
<Show
|
|
32
30
|
when={props.entry.principal.type === "user" ? props.entry.principal : undefined}
|
|
33
31
|
fallback={
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
props.entry.principal.type === "service_account" ? (
|
|
33
|
+
<CloudAvatar
|
|
34
|
+
username={props.label}
|
|
35
|
+
userId={props.entry.principal.serviceAccountId}
|
|
36
|
+
avatarHash={props.entry.avatarHash}
|
|
37
|
+
kind="service_account"
|
|
38
|
+
size="sm"
|
|
39
|
+
class="h-8 w-8"
|
|
40
|
+
/>
|
|
41
|
+
) : (
|
|
42
|
+
<span
|
|
43
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--ui-surface-muted)] text-secondary"
|
|
44
|
+
// The avatar formula: a light share of the hue on the surface, a dark share on the text, in both themes.
|
|
45
|
+
style={
|
|
46
|
+
hue()
|
|
47
|
+
? {
|
|
48
|
+
background: `color-mix(in srgb, ${hue()} 18%, var(--k2b-surface))`,
|
|
49
|
+
color: `color-mix(in srgb, ${hue()} 55%, var(--k2b-text))`,
|
|
50
|
+
}
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
>
|
|
55
|
+
<i class={`ti ${icon()} text-xs`} />
|
|
56
|
+
</span>
|
|
57
|
+
)
|
|
49
58
|
}
|
|
50
59
|
>
|
|
51
60
|
{(user) => (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Combobox, type ComboboxOption, useLocale } from "@k2b/ui";
|
|
2
|
-
import type { Principal, ServiceAccountKind } from "../contracts/shared";
|
|
2
|
+
import type { Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
|
|
3
3
|
import { accessMessages } from "./messages";
|
|
4
4
|
import { principalLabel } from "./principal-label";
|
|
5
5
|
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
@@ -34,7 +34,11 @@ export default function PrincipalPicker(props: {
|
|
|
34
34
|
existing?: Principal[];
|
|
35
35
|
allowPublic?: boolean;
|
|
36
36
|
allowAuthenticated?: boolean;
|
|
37
|
+
/** Defaults to true; disable only for a documented user-backed workflow. */
|
|
37
38
|
allowServiceAccounts?: boolean;
|
|
39
|
+
/** Optional directory filters for the resource's accepted credential kinds. */
|
|
40
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
41
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
38
42
|
/** Offers only the service accounts this returns `true` for, such as active accounts the resource can accept. */
|
|
39
43
|
serviceAccountFilter?: (account: PrincipalPickerServiceAccount) => boolean;
|
|
40
44
|
disabled?: boolean;
|
|
@@ -78,7 +82,13 @@ export default function PrincipalPicker(props: {
|
|
|
78
82
|
if (search.length >= 2) {
|
|
79
83
|
const url = new URL("/api/accounts/entities", window.location.origin);
|
|
80
84
|
url.searchParams.set("search", search);
|
|
81
|
-
url.searchParams.set("kinds", props.allowServiceAccounts ? "user,group,service_account" : "user,group");
|
|
85
|
+
url.searchParams.set("kinds", props.allowServiceAccounts !== false ? "user,group,service_account" : "user,group");
|
|
86
|
+
if (props.allowServiceAccounts !== false && props.serviceAccountKinds?.length) {
|
|
87
|
+
url.searchParams.set("service_account_kinds", props.serviceAccountKinds.join(","));
|
|
88
|
+
}
|
|
89
|
+
if (props.allowServiceAccounts !== false && props.serviceAccountStatus) {
|
|
90
|
+
url.searchParams.set("service_account_status", props.serviceAccountStatus);
|
|
91
|
+
}
|
|
82
92
|
url.searchParams.set("per_page", "10");
|
|
83
93
|
const users = props.existing?.flatMap((p) => (p.type === "user" ? [p.userId] : [])) ?? [];
|
|
84
94
|
const groups = props.existing?.flatMap((p) => (p.type === "group" ? [p.groupId] : [])) ?? [];
|
|
@@ -145,10 +155,10 @@ export default function PrincipalPicker(props: {
|
|
|
145
155
|
placeholder={
|
|
146
156
|
props.placeholder ??
|
|
147
157
|
(props.allowAuthenticated !== false || props.allowPublic
|
|
148
|
-
? props.allowServiceAccounts
|
|
158
|
+
? props.allowServiceAccounts !== false
|
|
149
159
|
? t().addAll
|
|
150
160
|
: t().addAudience
|
|
151
|
-
: props.allowServiceAccounts
|
|
161
|
+
: props.allowServiceAccounts !== false
|
|
152
162
|
? t().addService
|
|
153
163
|
: t().addBasic)
|
|
154
164
|
}
|
|
@@ -217,12 +217,11 @@ export default function ResourceApiKeys(props: ResourceApiKeysProps) {
|
|
|
217
217
|
if (confirmed) await revokeMutation.mutate({ id: key.id, name: key.name });
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
-
// The heading is a detail panel section heading: in a
|
|
221
|
-
// and below a settings group title they read as one level down.
|
|
220
|
+
// The heading is a detail panel section heading without an icon: in a details panel the keys line up with the
|
|
221
|
+
// sections around them, and below a settings group title they read as one level down. Keys are flat rows.
|
|
222
222
|
return (
|
|
223
223
|
<DetailPanel.Section
|
|
224
224
|
title={props.title ?? t().apiKeys}
|
|
225
|
-
icon="ti ti-key"
|
|
226
225
|
description={props.description ?? t().apiKeysDescription}
|
|
227
226
|
actions={
|
|
228
227
|
<Button type="button" variant="secondary" size="sm" onClick={openCreate} disabled={createMutation.loading()}>
|
|
@@ -235,7 +234,7 @@ export default function ResourceApiKeys(props: ResourceApiKeysProps) {
|
|
|
235
234
|
<div class="flex flex-col gap-2">
|
|
236
235
|
<For each={keys()}>
|
|
237
236
|
{(key) => (
|
|
238
|
-
<div class="group/api-key flex items-center gap-3
|
|
237
|
+
<div class="group/api-key flex items-center gap-3 py-1.5">
|
|
239
238
|
<div class="min-w-0 flex-1">
|
|
240
239
|
{/* The name and its tags wrap instead of squeezing the name to a few letters in a narrow section. */}
|
|
241
240
|
<div class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
@@ -55,12 +55,15 @@ export const appAccessMessages = i18n.define({
|
|
|
55
55
|
fixedNoGuests: "Signed-in people except guests. The app sets this rule; there is nothing to configure here.",
|
|
56
56
|
fixedAdmins: "Cloud administrators only. The app sets this rule; there is nothing to configure here.",
|
|
57
57
|
fixedRoles: "People with the Cloud roles required by the app. The app sets this rule.",
|
|
58
|
+
resources: "Accounts with an effective resource permission in this app. Manage permissions within the app.",
|
|
59
|
+
resourcesLabel: "Resource permissions",
|
|
58
60
|
legacy: "This app uses its existing access checks.",
|
|
59
61
|
deniedTitle: ({ name }: { name: string }) => `${name} is not available to you`,
|
|
60
62
|
revokedTitle: ({ name }: { name: string }) => `Your access to ${name} was removed`,
|
|
61
63
|
revokedDescription: "You had access until now. If this was a mistake, ask:",
|
|
62
64
|
deniedGrants: "Only people with access may use this app. Access is managed by:",
|
|
63
65
|
deniedFixed: "If you need this app, contact your Cloud administration.",
|
|
66
|
+
deniedResources: "Ask someone who manages resource permissions in this app for access.",
|
|
64
67
|
deniedAdmins: ({ name }: { name: string }) => `Only Cloud administrators use ${name}.`,
|
|
65
68
|
deniedNoGuests: ({ name }: { name: string }) => `Guests cannot use ${name}.`,
|
|
66
69
|
deniedRoles: ({ name }: { name: string }) => `Only people with specific Cloud roles use ${name}.`,
|
|
@@ -134,12 +137,15 @@ export const appAccessMessages = i18n.define({
|
|
|
134
137
|
fixedNoGuests: "Angemeldete Personen außer Gästen. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
|
|
135
138
|
fixedAdmins: "Nur Cloud-Admins. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
|
|
136
139
|
fixedRoles: "Personen mit den von der App verlangten Cloud-Rollen. Das legt die App fest.",
|
|
140
|
+
resources: "Konten mit einer wirksamen Ressourcenberechtigung in dieser App. Berechtigungen werden innerhalb der App verwaltet.",
|
|
141
|
+
resourcesLabel: "Ressourcenberechtigungen",
|
|
137
142
|
legacy: "Diese App verwendet ihre bisherigen Zugriffsprüfungen.",
|
|
138
143
|
deniedTitle: ({ name }: { name: string }) => `${name} ist für dich nicht freigegeben`,
|
|
139
144
|
revokedTitle: ({ name }: { name: string }) => `Dein Zugriff auf ${name} wurde entfernt`,
|
|
140
145
|
revokedDescription: "Du hattest bis eben Zugriff. Wenn das ein Versehen ist, frag:",
|
|
141
146
|
deniedGrants: "Diese App nutzen nur Personen mit Freigabe. Den Zugriff verwalten:",
|
|
142
147
|
deniedFixed: "Wenn du die App brauchst, wende dich an die Cloud-Verwaltung.",
|
|
148
|
+
deniedResources: "Bitte jemanden, der Ressourcenberechtigungen in dieser App verwaltet, um Zugriff.",
|
|
143
149
|
deniedAdmins: ({ name }: { name: string }) => `${name} nutzen nur Cloud-Admins.`,
|
|
144
150
|
deniedNoGuests: ({ name }: { name: string }) => `${name} nutzen nur angemeldete Personen außer Gästen.`,
|
|
145
151
|
deniedRoles: ({ name }: { name: string }) => `${name} nutzen nur Personen mit bestimmten Cloud-Rollen.`,
|
package/src/access/messages.ts
CHANGED
|
@@ -47,8 +47,8 @@ export const accessMessages = i18n.define({
|
|
|
47
47
|
count === 0
|
|
48
48
|
? "No one receives access through this group right now."
|
|
49
49
|
: count === 1
|
|
50
|
-
? "1
|
|
51
|
-
: `${count.toLocaleString("en")}
|
|
50
|
+
? "1 account receives access through this group right now."
|
|
51
|
+
: `${count.toLocaleString("en")} accounts receive access through this group right now.`,
|
|
52
52
|
groupMembersLoading: "Loading members…",
|
|
53
53
|
groupMembersHidden: "Your account can't see who belongs to this group.",
|
|
54
54
|
groupMembersFailed: "Members couldn't be loaded.",
|
|
@@ -137,8 +137,8 @@ export const accessMessages = i18n.define({
|
|
|
137
137
|
count === 0
|
|
138
138
|
? "Über diese Gruppe erhält gerade niemand Zugriff."
|
|
139
139
|
: count === 1
|
|
140
|
-
? "Über diese Gruppe erhält gerade 1
|
|
141
|
-
: `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")}
|
|
140
|
+
? "Über diese Gruppe erhält gerade 1 Konto Zugriff."
|
|
141
|
+
: `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")} Konten Zugriff.`,
|
|
142
142
|
groupMembersLoading: "Mitglieder werden geladen…",
|
|
143
143
|
groupMembersHidden: "Dein Konto kann nicht sehen, wer zu dieser Gruppe gehört.",
|
|
144
144
|
groupMembersFailed: "Die Mitglieder konnten nicht geladen werden.",
|
|
@@ -5,6 +5,7 @@ import { createSignal, For, Show } from "solid-js";
|
|
|
5
5
|
import { accessMessages } from "../access/messages";
|
|
6
6
|
import { principalLabel } from "../access/principal-label";
|
|
7
7
|
import { serviceAccountKindDisplay } from "../access/service-account-kind";
|
|
8
|
+
import type { ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
|
|
8
9
|
import { groupDisplayName } from "../shared/account-display";
|
|
9
10
|
import { CloudAvatar } from "./Avatar";
|
|
10
11
|
|
|
@@ -52,6 +53,10 @@ type EntitySearchProps = {
|
|
|
52
53
|
includeGroups?: boolean;
|
|
53
54
|
/** Surface service accounts in the result list. Off by default. */
|
|
54
55
|
includeServiceAccounts?: boolean;
|
|
56
|
+
/** Restrict service-account kinds before pagination. Omitted or empty allows all kinds. */
|
|
57
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
58
|
+
/** Restrict service-account status before pagination. Omitted allows both statuses. */
|
|
59
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
55
60
|
/** Inject a synthetic "All authenticated users" row at the top. */
|
|
56
61
|
includeAuthenticated?: boolean;
|
|
57
62
|
/** Inject a synthetic "Public" row at the top. */
|
|
@@ -171,6 +176,12 @@ const EntitySearch = (props: EntitySearchProps) => {
|
|
|
171
176
|
if (props.excludeServiceAccountIds?.length) {
|
|
172
177
|
url.searchParams.set("exclude_service_account_ids", props.excludeServiceAccountIds.join(","));
|
|
173
178
|
}
|
|
179
|
+
if (props.includeServiceAccounts && props.serviceAccountKinds?.length) {
|
|
180
|
+
url.searchParams.set("service_account_kinds", props.serviceAccountKinds.join(","));
|
|
181
|
+
}
|
|
182
|
+
if (props.includeServiceAccounts && props.serviceAccountStatus) {
|
|
183
|
+
url.searchParams.set("service_account_status", props.serviceAccountStatus);
|
|
184
|
+
}
|
|
174
185
|
if (props.onlyMembersOf?.length) {
|
|
175
186
|
url.searchParams.set("user_member_of_group_ids", props.onlyMembersOf.join(","));
|
|
176
187
|
}
|
|
@@ -296,6 +307,7 @@ const EntitySearch = (props: EntitySearchProps) => {
|
|
|
296
307
|
{(serviceAccount) => (
|
|
297
308
|
<ResultRow
|
|
298
309
|
icon={serviceAccountKind(serviceAccount).icon}
|
|
310
|
+
avatar={<CloudAvatar username={serviceAccount.name} userId={serviceAccount.id} kind="service_account" size="sm" />}
|
|
299
311
|
title={serviceAccount.name}
|
|
300
312
|
subtitle={
|
|
301
313
|
// Resource-bound accounts often share a name; their binding tells them apart.
|