@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
package/src/cli/admin/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { defineCliCommands } from "../commands";
|
|
|
9
9
|
import { accountAdministrationCommands } from "./account-administration";
|
|
10
10
|
import { accountCategoryCommands } from "./account-categories";
|
|
11
11
|
import { agentCommands } from "./agents";
|
|
12
|
+
import { aiModelCommands } from "./ai-models";
|
|
12
13
|
import { aiQuotaCommands } from "./ai-quotas";
|
|
13
14
|
import { aiSkillCommands } from "./ai-skills";
|
|
14
15
|
import { aiUsageCommands } from "./ai-usage";
|
|
@@ -39,8 +40,9 @@ export default defineCliCommands({
|
|
|
39
40
|
accounts: "Manage account policy and account administration.",
|
|
40
41
|
ai: "Manage AI models, quotas, Skills, and usage.",
|
|
41
42
|
"ai models": "Manage AI model configuration.",
|
|
42
|
-
"ai
|
|
43
|
-
"ai
|
|
43
|
+
"ai providers": "Manage AI provider connections.",
|
|
44
|
+
"ai variants": "Manage named model variants.",
|
|
45
|
+
"ai classes": "Manage model classes and tools.",
|
|
44
46
|
"ai quotas background": "Inspect and release the background emergency stop.",
|
|
45
47
|
"ai skills": "Inspect Assistant Skills and trusted templates.",
|
|
46
48
|
"ai usage": "Inspect AI runs and usage facets.",
|
|
@@ -94,6 +96,7 @@ export default defineCliCommands({
|
|
|
94
96
|
...accountCategoryCommands,
|
|
95
97
|
...accountAdministrationCommands,
|
|
96
98
|
...aiUsageCommands,
|
|
99
|
+
...aiModelCommands,
|
|
97
100
|
...aiQuotaCommands,
|
|
98
101
|
...aiSkillCommands,
|
|
99
102
|
...instanceCommands,
|
package/src/cli/apps.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppDetailsSchema } from "../contracts/app-details";
|
|
2
|
+
import { arg, type CloudCliContext, command, defineCliCommands, flag, localizeCloudCliText, printRows, printStructured } from "./index";
|
|
2
3
|
|
|
3
4
|
type VisibleApp = {
|
|
4
5
|
id: string;
|
|
@@ -13,28 +14,90 @@ const listApps = async (ctx: CloudCliContext, search: string | undefined) => {
|
|
|
13
14
|
return ctx.readJson<{ items: VisibleApp[] }>(await ctx.fetch(`/api/apps${query}`));
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
17
|
+
const build = (locale?: string) => {
|
|
18
|
+
const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
|
|
19
|
+
return defineCliCommands({
|
|
20
|
+
name: "apps",
|
|
21
|
+
summary: t("List Cloud apps and inspect their declarations.", "Cloud-Apps auflisten und ihre Deklarationen prüfen."),
|
|
22
|
+
commands: [
|
|
23
|
+
command("show", {
|
|
24
|
+
summary: t(
|
|
25
|
+
"Inspect one app's access, events and notifications (admin)",
|
|
26
|
+
"Zugriff, Events und Benachrichtigungen einer App prüfen (Admin)",
|
|
27
|
+
),
|
|
28
|
+
args: { app: arg.required({ description: t("App ID", "App-ID") }) },
|
|
29
|
+
run: async ({ ctx, args }) => {
|
|
30
|
+
const result = AppDetailsSchema.parse(
|
|
31
|
+
await ctx.readJson(await ctx.fetch(`/api/app-events/apps/${encodeURIComponent(args.app)}`)),
|
|
32
|
+
);
|
|
33
|
+
if (printStructured(ctx, result)) return;
|
|
34
|
+
const { app, access } = result;
|
|
35
|
+
const printOmitted = (shown: number, total: number) => {
|
|
36
|
+
if (total > shown) ctx.print(t(`… ${total - shown} more omitted`, `… ${total - shown} weitere ausgelassen`));
|
|
37
|
+
};
|
|
38
|
+
ctx.print(
|
|
39
|
+
`${t("App", "Anwendung")}: ${app.name} (${app.id}) · ${app.online ? t("online", "erreichbar") : t("offline", "nicht erreichbar")}`,
|
|
40
|
+
);
|
|
41
|
+
if (app.description) ctx.print(app.description);
|
|
42
|
+
ctx.print(`\n${t("Access", "Zugriff")}: ${access.kind ?? t("unknown (offline)", "unbekannt (offline)")}`);
|
|
43
|
+
if (access.roles.length) ctx.print(`${t("Roles", "Rollen")}: ${access.roles.join(", ")}`);
|
|
44
|
+
if (access.grantCount !== null) ctx.print(`${t("Grants", "Freigaben")}: ${access.grantCount}`);
|
|
45
|
+
ctx.print(`${t("Platform permissions", "Plattformrechte")}: ${access.platformPermissions.join(", ") || "—"}`);
|
|
46
|
+
ctx.print(`\n${t("Sends", "Sendet")} (${result.sends.length}/${result.totals.sends})`);
|
|
47
|
+
for (const event of result.sends) {
|
|
48
|
+
ctx.print(`${event.type} v${event.version} · ${event.title} · ${event.subscriberCount} ${t("subscribers", "Abonnenten")}`);
|
|
49
|
+
ctx.print(
|
|
50
|
+
` ${event.subject}${event.place ? ` @ ${event.place}` : ""} · ${t("automation", "Automatisierung")}=${event.automation ? t("yes", "ja") : t("no", "nein")}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
printOmitted(result.sends.length, result.totals.sends);
|
|
54
|
+
ctx.print(`\n${t("Hears", "Hört")} (${result.hears.length}/${result.totals.hears})`);
|
|
55
|
+
for (const subscription of result.hears) {
|
|
56
|
+
ctx.print(
|
|
57
|
+
`${subscription.eventType} v${subscription.version} · ${subscription.state}${subscription.isNew ? ` · ${t("new", "neu")}` : ""} · ${subscription.counts.failed} ${t("failed", "fehlgeschlagen")}`,
|
|
58
|
+
);
|
|
59
|
+
ctx.print(` ${subscription.subscriptionId}${subscription.reason ? ` · ${subscription.reason}` : ""}`);
|
|
60
|
+
for (const failure of subscription.failedDeliveries)
|
|
61
|
+
ctx.print(
|
|
62
|
+
` ${failure.id} · ${failure.code ?? t("failed", "fehlgeschlagen")} · ${failure.attempts} ${t("attempts", "Versuche")}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
printOmitted(result.hears.length, result.totals.hears);
|
|
66
|
+
ctx.print(`\n${t("Notifications", "Benachrichtigungen")} (${result.notifications.length}/${result.totals.notifications})`);
|
|
67
|
+
for (const notification of result.notifications) {
|
|
68
|
+
ctx.print(`${notification.id} · ${notification.label} · ${notification.recipientKind}`);
|
|
69
|
+
ctx.print(
|
|
70
|
+
` ${t("Required", "Erforderlich")}: ${notification.requiredChannels.join(", ") || "—"}; ${t("recommended", "empfohlen")}: ${notification.recommendedChannels.join(", ") || "—"}; ${t("excluded", "ausgeschlossen")}: ${notification.excludedChannels.join(", ") || "—"}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
printOmitted(result.notifications.length, result.totals.notifications);
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
command("list", {
|
|
77
|
+
summary: t("List available Cloud apps", "Verfügbare Cloud-Apps auflisten"),
|
|
78
|
+
flags: {
|
|
79
|
+
search: flag.string({
|
|
80
|
+
aliases: ["q"],
|
|
81
|
+
description: t("Filter by app name, id, or description", "Nach App-Name, ID oder Beschreibung filtern"),
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
run: async ({ ctx, flags }) => {
|
|
85
|
+
const result = await listApps(ctx, flags.search);
|
|
86
|
+
printRows(ctx, result, result.items, [
|
|
87
|
+
{ key: "id", label: "ID" },
|
|
88
|
+
{ key: "name", label: "NAME" },
|
|
89
|
+
{ key: "description", label: t("DESCRIPTION", "BESCHREIBUNG") },
|
|
90
|
+
{ key: "href", label: "URL" },
|
|
91
|
+
]);
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const appsCliModule = build();
|
|
99
|
+
export default {
|
|
100
|
+
...appsCliModule,
|
|
101
|
+
help: (locale?: string) => build(locale).help?.(locale) ?? "",
|
|
102
|
+
run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
|
|
103
|
+
};
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
cloudResourceRefAppId,
|
|
17
17
|
resolveCapabilityResourceReader,
|
|
18
18
|
} from "../contracts/capabilities";
|
|
19
|
+
import { ResolvedCloudResourceUrlSchema } from "../contracts/resource-resolution";
|
|
19
20
|
import { saveCapabilityStream } from "./capability-streams";
|
|
20
21
|
import { arg, type CliInputFlagValue, type CloudCliContext, command, defineCliCommands, flag, readCliInput } from "./index";
|
|
21
22
|
import { cloudResourceUrl } from "./resource-links";
|
|
@@ -139,7 +140,7 @@ const invoke = async (config: {
|
|
|
139
140
|
}
|
|
140
141
|
throw error;
|
|
141
142
|
}
|
|
142
|
-
const result = capabilityResultSchema(z.unknown()).parse(body);
|
|
143
|
+
const result = capabilityResultSchema(z.unknown(), { consumer: true }).parse(body);
|
|
143
144
|
const manifests = config.manifest ? [config.manifest] : [];
|
|
144
145
|
const wantedApps = new Set((result.refs ?? []).map(cloudResourceRefAppId));
|
|
145
146
|
for (const manifest of manifests) wantedApps.delete(manifest.appId);
|
|
@@ -179,6 +180,22 @@ export default defineCliCommands({
|
|
|
179
180
|
name: "capabilities",
|
|
180
181
|
summary: "Discover and invoke versioned Cloud app capabilities.",
|
|
181
182
|
commands: [
|
|
183
|
+
command("resolve", {
|
|
184
|
+
summary: "Recognize a Cloud link and read its resource reference",
|
|
185
|
+
args: { url: arg.required({ description: "Cloud permalink or application page URL" }) },
|
|
186
|
+
run: async ({ ctx, args }) => {
|
|
187
|
+
const response = await ctx.fetch("/api/resources/resolve", {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "content-type": "application/json" },
|
|
190
|
+
body: JSON.stringify({ url: args.url.trim() }),
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) throw new Error("Resource not found.");
|
|
193
|
+
const body = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
|
|
194
|
+
if (!body.ok) throw new Error("Resource not found.");
|
|
195
|
+
const result = ResolvedCloudResourceUrlSchema.parse(body.data);
|
|
196
|
+
printGenericResult(ctx, { ...result, permalink: cloudResourceUrl(ctx, result.ref) });
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
182
199
|
...(["read", "write", "status", "abort"] as const).map((verb) =>
|
|
183
200
|
command(`stream-${verb}`, {
|
|
184
201
|
summary: `${verb} an authenticated binary capability stream`,
|
|
@@ -247,6 +264,8 @@ export default defineCliCommands({
|
|
|
247
264
|
types: app.manifest.types.length,
|
|
248
265
|
queries: app.manifest.queries.length,
|
|
249
266
|
actions: app.manifest.actions.length,
|
|
267
|
+
events: app.manifest.events?.length ?? 0,
|
|
268
|
+
subscribes: app.manifest.subscribes?.length ?? 0,
|
|
250
269
|
})),
|
|
251
270
|
[
|
|
252
271
|
{ key: "app", label: "APP" },
|
|
@@ -254,6 +273,8 @@ export default defineCliCommands({
|
|
|
254
273
|
{ key: "types", label: "TYPES" },
|
|
255
274
|
{ key: "queries", label: "QUERIES" },
|
|
256
275
|
{ key: "actions", label: "ACTIONS" },
|
|
276
|
+
{ key: "events", label: "EVENTS" },
|
|
277
|
+
{ key: "subscribes", label: "SUBSCRIBES" },
|
|
257
278
|
],
|
|
258
279
|
);
|
|
259
280
|
if (result.page.hasMore) ctx.error(`More apps available; continue with --cursor ${result.page.nextCursor}`);
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { link, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { assertAppEventSize } from "../_internal/app-event-schemas";
|
|
6
|
+
import { APP_EVENT_MAX_BYTES, AppEventEnvelopeSchema, appEventSubscriptionId } from "../contracts/app-events";
|
|
7
|
+
import { CapabilityQualifiedIdSchema } from "../contracts/capabilities";
|
|
8
|
+
import { arg, command, confirmFlag, defineCliCommands, flag } from "./commands";
|
|
9
|
+
import type { CloudCliContext } from "./index";
|
|
10
|
+
import { localizeCloudCliText } from "./locale";
|
|
11
|
+
import { printRows, printStructured } from "./output";
|
|
12
|
+
|
|
13
|
+
const API = "/api/app-events";
|
|
14
|
+
const catalogSchema = z.object({
|
|
15
|
+
events: z.array(
|
|
16
|
+
z.object({
|
|
17
|
+
type: CapabilityQualifiedIdSchema,
|
|
18
|
+
version: z.number().int().positive(),
|
|
19
|
+
title: z.string(),
|
|
20
|
+
description: z.string(),
|
|
21
|
+
subject: CapabilityQualifiedIdSchema,
|
|
22
|
+
place: CapabilityQualifiedIdSchema.nullable(),
|
|
23
|
+
automation: z.boolean(),
|
|
24
|
+
dataSchema: z.record(z.string(), z.unknown()),
|
|
25
|
+
subscribers: z.array(z.unknown()),
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const read = async <T>(ctx: CloudCliContext, path: string, init?: RequestInit) => ctx.readJson<T>(await ctx.fetch(`${API}${path}`, init));
|
|
31
|
+
const catalog = async (ctx: CloudCliContext) => catalogSchema.parse(await read(ctx, "/catalog"));
|
|
32
|
+
|
|
33
|
+
// Consume one bounded envelope at a time, including when the input has no newline.
|
|
34
|
+
async function* jsonLines(stream: ReadableStream<Uint8Array>, locale?: string, maxBytes?: number) {
|
|
35
|
+
const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
|
|
36
|
+
const reader = stream.getReader();
|
|
37
|
+
let pending = new Uint8Array(0);
|
|
38
|
+
let line = 0;
|
|
39
|
+
let bytesRead = 0;
|
|
40
|
+
const decode = (bytes: Uint8Array) => {
|
|
41
|
+
line++;
|
|
42
|
+
try {
|
|
43
|
+
const envelope = AppEventEnvelopeSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
44
|
+
assertAppEventSize(envelope);
|
|
45
|
+
return envelope;
|
|
46
|
+
} catch (cause) {
|
|
47
|
+
throw new Error(t(`Invalid event fixture at line ${line}.`, `Ungültige Event-Fixture in Zeile ${line}.`), { cause });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
while (true) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) break;
|
|
54
|
+
bytesRead += value.byteLength;
|
|
55
|
+
if (maxBytes !== undefined && bytesRead > maxBytes) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
t("Fixture response exceeds the requested recording budget.", "Die Fixture-Antwort überschreitet das Aufnahmebudget."),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
let start = 0;
|
|
61
|
+
for (let index = 0; index <= value.length; index++) {
|
|
62
|
+
if (index < value.length && value[index] !== 10) continue;
|
|
63
|
+
const part = value.subarray(start, index);
|
|
64
|
+
if (pending.length + part.length > APP_EVENT_MAX_BYTES) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
t(
|
|
67
|
+
`Event fixture line ${line + 1} exceeds ${APP_EVENT_MAX_BYTES} bytes.`,
|
|
68
|
+
`Event-Fixture in Zeile ${line + 1} überschreitet ${APP_EVENT_MAX_BYTES} Bytes.`,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const next = new Uint8Array(pending.length + part.length);
|
|
73
|
+
next.set(pending);
|
|
74
|
+
next.set(part, pending.length);
|
|
75
|
+
pending = next;
|
|
76
|
+
if (index < value.length) {
|
|
77
|
+
if (pending.length) yield decode(pending);
|
|
78
|
+
else line++;
|
|
79
|
+
pending = new Uint8Array(0);
|
|
80
|
+
}
|
|
81
|
+
start = index + 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (pending.length) yield decode(pending);
|
|
85
|
+
} finally {
|
|
86
|
+
await reader.cancel();
|
|
87
|
+
reader.releaseLock();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const build = (locale?: string) => {
|
|
92
|
+
const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
|
|
93
|
+
const print = (ctx: CloudCliContext, value: unknown) => {
|
|
94
|
+
if (!printStructured(ctx, value)) ctx.print(JSON.stringify(value, null, 2));
|
|
95
|
+
};
|
|
96
|
+
return defineCliCommands({
|
|
97
|
+
name: "events",
|
|
98
|
+
summary: t("Inspect declared app events and test local handlers.", "Deklarierte App-Events prüfen und lokale Handler testen."),
|
|
99
|
+
commands: [
|
|
100
|
+
command("ls", {
|
|
101
|
+
summary: t("List declared event types", "Deklarierte Event-Typen auflisten"),
|
|
102
|
+
flags: {
|
|
103
|
+
app: flag.string({ description: t("Publisher application ID", "ID der veröffentlichenden Anwendung") }),
|
|
104
|
+
automation: flag.enum(["true", "false"], {
|
|
105
|
+
description: t("Filter by automation metadata", "Nach Automationsmetadaten filtern"),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
async run({ ctx, flags }) {
|
|
109
|
+
const result = await catalog(ctx);
|
|
110
|
+
const events = result.events.filter(
|
|
111
|
+
(event) =>
|
|
112
|
+
(!flags.app || event.type.startsWith(`${flags.app}.`)) &&
|
|
113
|
+
(flags.automation === undefined || event.automation === (flags.automation === "true")),
|
|
114
|
+
);
|
|
115
|
+
printRows(ctx, { events }, events, [
|
|
116
|
+
{ key: "type", label: "TYPE" },
|
|
117
|
+
{ key: "version", label: "VERSION" },
|
|
118
|
+
{ key: "title", label: t("TITLE", "TITEL") },
|
|
119
|
+
{ key: "subject", label: "SUBJECT" },
|
|
120
|
+
{ key: "place", label: "PLACE", value: (event) => event.place ?? "—" },
|
|
121
|
+
{ key: "automation", label: "AUTOMATION", value: (event) => (event.automation ? t("yes", "ja") : t("no", "nein")) },
|
|
122
|
+
]);
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
command("subscriptions", {
|
|
126
|
+
summary: t("Inspect subscriptions and blocked reasons", "Abonnements und Sperrgründe prüfen"),
|
|
127
|
+
flags: { app: flag.string({ description: t("Subscriber application ID", "ID der abonnierenden Anwendung") }) },
|
|
128
|
+
run: async ({ ctx, flags }) =>
|
|
129
|
+
print(ctx, await read(ctx, `/subscriptions${flags.app ? `?${new URLSearchParams({ app: flags.app })}` : ""}`)),
|
|
130
|
+
}),
|
|
131
|
+
command("status", {
|
|
132
|
+
summary: t("Show event broker status and rejected events", "Status des Event-Brokers und abgelehnte Events anzeigen"),
|
|
133
|
+
run: async ({ ctx }) => print(ctx, await read(ctx, "/status")),
|
|
134
|
+
}),
|
|
135
|
+
command("deliveries", {
|
|
136
|
+
summary: t("List event deliveries", "Event-Zustellungen auflisten"),
|
|
137
|
+
flags: {
|
|
138
|
+
app: flag.string({ description: t("Subscriber application ID", "ID der abonnierenden Anwendung") }),
|
|
139
|
+
state: flag.string({ description: t("Delivery state", "Zustellstatus") }),
|
|
140
|
+
eventType: flag.string({ description: t("Qualified event type", "Vollständiger Event-Typ") }),
|
|
141
|
+
offset: flag.int({ default: 0, min: 0, description: t("Pagination offset", "Startposition der Seite") }),
|
|
142
|
+
limit: flag.int({ default: 50, min: 1, max: 100, description: t("Maximum deliveries", "Maximale Anzahl Zustellungen") }),
|
|
143
|
+
},
|
|
144
|
+
async run({ ctx, flags }) {
|
|
145
|
+
const query = new URLSearchParams({ offset: String(flags.offset), limit: String(flags.limit) });
|
|
146
|
+
for (const key of ["app", "state", "eventType"] as const) if (flags[key]) query.set(key, flags[key]);
|
|
147
|
+
const result = await read<{
|
|
148
|
+
items: Record<string, unknown>[];
|
|
149
|
+
pagination: { offset: number; limit: number; total: number };
|
|
150
|
+
}>(ctx, `/deliveries?${query}`);
|
|
151
|
+
printRows(ctx, result, result.items, [
|
|
152
|
+
{ key: "id", label: "ID" },
|
|
153
|
+
{ key: "subscriptionId", label: "SUBSCRIPTION" },
|
|
154
|
+
{ key: "state", label: t("STATE", "STATUS") },
|
|
155
|
+
{ key: "attempts", label: t("ATTEMPTS", "VERSUCHE") },
|
|
156
|
+
{ key: "heldBy", label: t("HELD BY", "WARTET AUF") },
|
|
157
|
+
]);
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
command("redeliver", {
|
|
161
|
+
summary: t("Redeliver one retained delivery", "Eine gespeicherte Zustellung erneut ausführen"),
|
|
162
|
+
args: { id: arg.required({ description: t("Delivery ID", "Zustellungs-ID") }) },
|
|
163
|
+
flags: { yes: confirmFlag() },
|
|
164
|
+
async run({ ctx, args, flags }) {
|
|
165
|
+
if (!flags.yes) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
t(
|
|
168
|
+
"Pass --yes to redeliver; the handler may repeat effects.",
|
|
169
|
+
"Für die erneute Zustellung --yes angeben; der Handler kann Effekte wiederholen.",
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
print(ctx, await read(ctx, `/deliveries/${encodeURIComponent(args.id)}/redeliver`, { method: "POST" }));
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
command("acknowledge-gap", {
|
|
177
|
+
summary: t("Acknowledge a reconciled retention gap", "Eine abgeglichene Retention-Lücke bestätigen"),
|
|
178
|
+
flags: { yes: confirmFlag() },
|
|
179
|
+
async run({ ctx, flags }) {
|
|
180
|
+
if (!flags.yes)
|
|
181
|
+
throw new Error(
|
|
182
|
+
t("Pass --yes after reconciling affected subscribers.", "Nach dem Abgleich betroffener Abonnenten --yes angeben."),
|
|
183
|
+
);
|
|
184
|
+
print(ctx, await read(ctx, "/status/acknowledge", { method: "POST" }));
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
command("record", {
|
|
188
|
+
summary: t("Export shape-only JSONL fixtures", "JSONL-Fixtures nur mit synthetischen Daten exportieren"),
|
|
189
|
+
args: { eventType: arg.required({ description: t("Qualified event type", "Vollständiger Event-Typ") }) },
|
|
190
|
+
flags: {
|
|
191
|
+
shapeOnly: flag.boolean({
|
|
192
|
+
description: t("Required: generate synthetic fixture data", "Erforderlich: synthetische Fixture-Daten erzeugen"),
|
|
193
|
+
}),
|
|
194
|
+
limit: flag.int({
|
|
195
|
+
default: 10,
|
|
196
|
+
min: 1,
|
|
197
|
+
max: 100,
|
|
198
|
+
description: t("Recent events to inspect for distinct shapes", "Neueste Events für unterschiedliche Strukturen prüfen"),
|
|
199
|
+
}),
|
|
200
|
+
out: flag.string({ description: t("Create a JSONL file", "JSONL-Datei erstellen") }),
|
|
201
|
+
},
|
|
202
|
+
async run({ ctx, args, flags }) {
|
|
203
|
+
if (!flags.shapeOnly) throw new Error(t("record requires --shape-only.", "record erfordert --shape-only."));
|
|
204
|
+
const query = new URLSearchParams({ shapeOnly: "true", limit: String(flags.limit) });
|
|
205
|
+
const response = await ctx.fetch(`${API}/record/${encodeURIComponent(args.eventType)}?${query}`);
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
await ctx.readJson(response);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!response.body) throw new Error(t("Cloud returned no fixture stream.", "Cloud hat keinen Fixture-Stream geliefert."));
|
|
211
|
+
const fixtures = [];
|
|
212
|
+
for await (const envelope of jsonLines(response.body, locale, flags.limit! * (APP_EVENT_MAX_BYTES + 1))) {
|
|
213
|
+
if (fixtures.length >= flags.limit!) {
|
|
214
|
+
throw new Error(t("Cloud returned too many fixtures.", "Cloud hat zu viele Fixtures geliefert."));
|
|
215
|
+
}
|
|
216
|
+
if (envelope.type !== args.eventType) {
|
|
217
|
+
throw new Error(t("Cloud returned a different event type.", "Cloud hat einen anderen Event-Typ geliefert."));
|
|
218
|
+
}
|
|
219
|
+
fixtures.push(envelope);
|
|
220
|
+
}
|
|
221
|
+
if (flags.out) {
|
|
222
|
+
const path = resolve(flags.out);
|
|
223
|
+
const temporary = await mkdtemp(join(dirname(path), ".cloud-event-fixtures-"));
|
|
224
|
+
try {
|
|
225
|
+
const part = join(temporary, "fixtures.jsonl");
|
|
226
|
+
await writeFile(part, fixtures.map((event) => JSON.stringify(event)).join("\n") + (fixtures.length ? "\n" : ""), {
|
|
227
|
+
mode: 0o600,
|
|
228
|
+
});
|
|
229
|
+
await link(part, path);
|
|
230
|
+
} finally {
|
|
231
|
+
await rm(temporary, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
print(ctx, { path, count: fixtures.length });
|
|
234
|
+
} else if (ctx.options.output === "json") ctx.json(fixtures);
|
|
235
|
+
else for (const fixture of fixtures) await ctx.write(`${JSON.stringify(fixture)}\n`);
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
command("replay", {
|
|
239
|
+
summary: t("Replay fixtures into a local handler", "Fixtures an einen lokalen Handler übergeben"),
|
|
240
|
+
args: { fixtures: arg.required({ description: t("JSONL fixture file", "JSONL-Fixture-Datei") }) },
|
|
241
|
+
flags: {
|
|
242
|
+
handler: flag.string({
|
|
243
|
+
required: true,
|
|
244
|
+
description: t("Local module path[#export], default export by default", "Lokaler Modulpfad[#export], sonst Default-Export"),
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
async run({ ctx, args, flags }) {
|
|
248
|
+
const current = await catalog(ctx);
|
|
249
|
+
const declarations = new Map(current.events.map((event) => [event.type, event]));
|
|
250
|
+
const validators = new Map<string, z.ZodType>();
|
|
251
|
+
const [modulePath, exportName = "default", ...extra] = flags.handler!.split("#");
|
|
252
|
+
if (!modulePath || !exportName || extra.length) {
|
|
253
|
+
throw new Error(t("Use a local path[#export] for --handler.", "Für --handler einen lokalen Pfad[#export] angeben."));
|
|
254
|
+
}
|
|
255
|
+
const module: Record<string, unknown> = await import(pathToFileURL(resolve(modulePath)).href);
|
|
256
|
+
const handler = module[exportName];
|
|
257
|
+
if (typeof handler !== "function") {
|
|
258
|
+
throw new Error(t("The selected handler export must be a function.", "Der gewählte Handler-Export muss eine Funktion sein."));
|
|
259
|
+
}
|
|
260
|
+
let delivered = 0;
|
|
261
|
+
try {
|
|
262
|
+
for await (const envelope of jsonLines(Bun.file(resolve(args.fixtures)).stream(), locale)) {
|
|
263
|
+
const event = declarations.get(envelope.type);
|
|
264
|
+
if (!event || event.version !== envelope.version) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
t(
|
|
267
|
+
`Event ${envelope.type}@${envelope.version} is absent from the current catalog.`,
|
|
268
|
+
`Event ${envelope.type}@${envelope.version} fehlt im aktuellen Katalog.`,
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
envelope.source !== envelope.type.split(".")[0] ||
|
|
274
|
+
envelope.subject.type !== event.subject ||
|
|
275
|
+
(envelope.place?.type ?? null) !== event.place
|
|
276
|
+
) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
t(
|
|
279
|
+
`Event ${envelope.type} references do not match the current catalog.`,
|
|
280
|
+
`Referenzen von Event ${envelope.type} passen nicht zum aktuellen Katalog.`,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
let validator = validators.get(envelope.type);
|
|
285
|
+
if (!validator) {
|
|
286
|
+
validator = z.fromJSONSchema(structuredClone(event.dataSchema));
|
|
287
|
+
validators.set(envelope.type, validator);
|
|
288
|
+
}
|
|
289
|
+
validator.parse(envelope.data);
|
|
290
|
+
await handler(envelope, {
|
|
291
|
+
subscriptionId: appEventSubscriptionId("replay", envelope.type, envelope.version),
|
|
292
|
+
eventId: envelope.id,
|
|
293
|
+
attempt: 1,
|
|
294
|
+
signal: new AbortController().signal,
|
|
295
|
+
});
|
|
296
|
+
delivered++;
|
|
297
|
+
}
|
|
298
|
+
} catch (cause) {
|
|
299
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
300
|
+
throw new Error(
|
|
301
|
+
t(
|
|
302
|
+
`Replay stopped after ${delivered} completed fixtures: ${detail}`,
|
|
303
|
+
`Replay wurde nach ${delivered} abgeschlossenen Fixtures angehalten: ${detail}`,
|
|
304
|
+
),
|
|
305
|
+
{ cause },
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
print(ctx, { delivered });
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const eventsCliModule = build();
|
|
316
|
+
export default {
|
|
317
|
+
...eventsCliModule,
|
|
318
|
+
help: (locale?: string) => build(locale).help?.(locale) ?? "",
|
|
319
|
+
run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
|
|
320
|
+
};
|
package/src/config/env.ts
CHANGED
|
@@ -10,6 +10,17 @@ const syncReplicas = z
|
|
|
10
10
|
.enum(["1", "3", "5"], { error: "must be 1, 3, or 5" })
|
|
11
11
|
.transform((value): 1 | 3 | 5 => (value === "1" ? 1 : value === "3" ? 3 : 5));
|
|
12
12
|
|
|
13
|
+
const budgetSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
maxStepsPerSource: z.number().int().positive(),
|
|
16
|
+
tasksPerMinute: z.number().int().positive(),
|
|
17
|
+
burst: z.number().int().positive(),
|
|
18
|
+
sourcesPer24Hours: z.number().int().positive(),
|
|
19
|
+
scopeConcurrent: z.number().int().positive(),
|
|
20
|
+
globalConcurrent: z.number().int().positive(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
|
|
13
24
|
const registry = defineEnv({
|
|
14
25
|
// Process
|
|
15
26
|
NODE_ENV: {
|
|
@@ -66,6 +77,35 @@ const registry = defineEnv({
|
|
|
66
77
|
doc: "Only dial the listed servers and ignore cluster-advertised peer addresses; set to `false` when every advertised address is reachable.",
|
|
67
78
|
},
|
|
68
79
|
|
|
80
|
+
WORKFLOW_AI_BUDGET: {
|
|
81
|
+
schema: z
|
|
82
|
+
.string()
|
|
83
|
+
.transform((value, ctx) => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(value);
|
|
86
|
+
} catch {
|
|
87
|
+
ctx.addIssue({ code: "custom", message: "must be JSON" });
|
|
88
|
+
return z.NEVER;
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.pipe(budgetSchema),
|
|
92
|
+
default: { maxStepsPerSource: 2, tasksPerMinute: 10, burst: 20, sourcesPer24Hours: 2_000, scopeConcurrent: 1, globalConcurrent: 4 },
|
|
93
|
+
doc: "JSON scoped workflow inference budget: active or successful logical requests per source/24h, token-bucket admission per minute and burst, distinct sources per rolling 24h, outstanding tasks per scope and globally; capacity/rate overload uses bounded workflow retries, daily/source limits fail visibly, and cache hits consume no reservation.",
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Workflow inference: p95 ~4s in the pilot; 60s bounds a whole attempt,
|
|
97
|
+
// including admission and structured-output repair, not just stream idleness.
|
|
98
|
+
WORKFLOW_AI_ATTEMPT_TIMEOUT_MS: {
|
|
99
|
+
schema: z.coerce.number().int().positive(),
|
|
100
|
+
default: 60_000,
|
|
101
|
+
doc: "Absolute workflow AI attempt budget in milliseconds, including admission and repair; expiry aborts the provider and follows the bounded retry policy.",
|
|
102
|
+
},
|
|
103
|
+
WORKFLOW_AI_QUEUE_TIMEOUT_MS: {
|
|
104
|
+
schema: z.coerce.number().int().positive(),
|
|
105
|
+
default: 60_000,
|
|
106
|
+
doc: "Maximum workflow AI queue wait in milliseconds; recovery fails overdue or lost deliveries visibly so an operator can retry the workflow.",
|
|
107
|
+
},
|
|
108
|
+
|
|
69
109
|
// Installation
|
|
70
110
|
APP_URL: {
|
|
71
111
|
schema: z.string(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
4
|
+
import type { ResourceAccessAdapter } from "../server/services/access";
|
|
4
5
|
import { AccessEntrySchema, type Role, RoleSchema } from "./shared";
|
|
5
6
|
|
|
6
7
|
const publicRoutes = z
|
|
@@ -22,6 +23,7 @@ export const AppAccessRuleSchema = z.union([
|
|
|
22
23
|
z.object({ kind: z.literal("signedIn"), publicRoutes }).strict(),
|
|
23
24
|
z.object({ kind: z.literal("noGuests"), publicRoutes }).strict(),
|
|
24
25
|
z.object({ kind: z.literal("roles"), roles: z.array(RoleSchema).min(1).readonly(), publicRoutes }).strict(),
|
|
26
|
+
z.object({ kind: z.literal("resources"), adapters: z.array(z.string().min(1)).min(1).readonly(), publicRoutes }).strict(),
|
|
25
27
|
z.object({ kind: z.literal("grants"), until: FixedAppAccessRuleSchema.optional(), publicRoutes }).strict(),
|
|
26
28
|
]);
|
|
27
29
|
/** Serializable entry rule evaluated by the platform; never application callback code. */
|
|
@@ -30,17 +32,32 @@ export type AppAccessRule = z.infer<typeof AppAccessRuleSchema>;
|
|
|
30
32
|
* The optional management restriction runs in the owning application, in addition to a management grant.
|
|
31
33
|
* SQL-backed callbacks and domain services they call must use the supplied db: edits hold its transaction connection.
|
|
32
34
|
*/
|
|
33
|
-
export type AppAccessDeclaration =
|
|
35
|
+
export type AppAccessDeclaration = (
|
|
36
|
+
| Exclude<AppAccessRule, { kind: "resources" }>
|
|
37
|
+
| {
|
|
38
|
+
kind: "resources";
|
|
39
|
+
adapters: Readonly<Record<string, ResourceAppAccessAdapter>>;
|
|
40
|
+
publicRoutes?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
) & {
|
|
34
43
|
canManage?: (authority: RequestAuthority, context: { db: SQL }) => boolean | Promise<boolean>;
|
|
35
44
|
};
|
|
45
|
+
/** An existence query is required for resource-derived app access; list() cannot answer it without a resource scan. */
|
|
46
|
+
export type ResourceAppAccessAdapter = Required<Pick<ResourceAccessAdapter, "hasAnyRight">>;
|
|
36
47
|
type PublicOptions = { publicRoutes?: readonly string[] };
|
|
37
48
|
export const appAccess = {
|
|
49
|
+
resources: (
|
|
50
|
+
options: PublicOptions & { adapters: Readonly<Record<string, ResourceAppAccessAdapter>> },
|
|
51
|
+
): Extract<AppAccessDeclaration, { kind: "resources" }> => ({
|
|
52
|
+
kind: "resources",
|
|
53
|
+
...options,
|
|
54
|
+
}),
|
|
38
55
|
signedIn: (options: PublicOptions = {}): AppAccessRule & { kind: "signedIn" } => ({ kind: "signedIn", ...options }),
|
|
39
56
|
noGuests: (options: PublicOptions = {}): AppAccessRule & { kind: "noGuests" } => ({ kind: "noGuests", ...options }),
|
|
40
57
|
roles: (roles: readonly Role[], options: PublicOptions = {}): AppAccessRule & { kind: "roles" } => ({ kind: "roles", roles, ...options }),
|
|
41
58
|
grants: (
|
|
42
59
|
options: PublicOptions & { until?: FixedAppAccessRule; canManage?: AppAccessDeclaration["canManage"] } = {},
|
|
43
|
-
): AppAccessDeclaration => ({ kind: "grants", ...options }),
|
|
60
|
+
): Extract<AppAccessDeclaration, { kind: "grants" }> => ({ kind: "grants", ...options }),
|
|
44
61
|
};
|
|
45
62
|
|
|
46
63
|
export const compileAppAccess = (declaration: AppAccessDeclaration | undefined): AppAccessRule | undefined => {
|
|
@@ -48,6 +65,13 @@ export const compileAppAccess = (declaration: AppAccessDeclaration | undefined):
|
|
|
48
65
|
const { canManage, ...rule } = declaration;
|
|
49
66
|
if (canManage && (rule.kind !== "grants" || typeof canManage !== "function"))
|
|
50
67
|
throw new Error("Only configurable app access accepts canManage");
|
|
68
|
+
if (rule.kind === "resources") {
|
|
69
|
+
if (Array.isArray(rule.adapters)) throw new Error("Declare resource adapters, not registry metadata");
|
|
70
|
+
for (const adapter of Object.values(rule.adapters)) {
|
|
71
|
+
if (typeof adapter.hasAnyRight !== "function") throw new Error("Resource app access requires hasAnyRight on every adapter");
|
|
72
|
+
}
|
|
73
|
+
return AppAccessRuleSchema.parse({ ...rule, adapters: Object.keys(rule.adapters) });
|
|
74
|
+
}
|
|
51
75
|
return AppAccessRuleSchema.parse(rule);
|
|
52
76
|
};
|
|
53
77
|
|