@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,224 @@
|
|
|
1
|
+
import { type SQL, sql } from "bun";
|
|
2
|
+
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
3
|
+
import { getApp, listCapabilities } from "../_internal/registry";
|
|
4
|
+
import {
|
|
5
|
+
APP_DETAILS_FAILED_LIMIT,
|
|
6
|
+
APP_DETAILS_LIST_LIMIT,
|
|
7
|
+
APP_SUBSCRIPTION_NEW_WINDOW_MS,
|
|
8
|
+
type AppDetails,
|
|
9
|
+
} from "../contracts/app-details";
|
|
10
|
+
import { type NotificationDefinitionPresentationCatalog, resolveNotificationDefinitionPresentation } from "../contracts/notification-types";
|
|
11
|
+
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
12
|
+
import type { RequestAuthority } from "../server/middleware/auth";
|
|
13
|
+
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
14
|
+
import { AppAccessManagementError, appAccessGrantCounts, canRepairAppAccess } from "./app-access-management";
|
|
15
|
+
import type { StoredDeclaration } from "./app-events/declarations";
|
|
16
|
+
|
|
17
|
+
type SubscriptionRow = {
|
|
18
|
+
id: string;
|
|
19
|
+
event_type: string;
|
|
20
|
+
version: number;
|
|
21
|
+
state: AppDetails["hears"][number]["state"];
|
|
22
|
+
reason: string | null;
|
|
23
|
+
first_seen_at: Date;
|
|
24
|
+
is_new: boolean;
|
|
25
|
+
publisher_app_id: string | null;
|
|
26
|
+
title: string | null;
|
|
27
|
+
total: number;
|
|
28
|
+
};
|
|
29
|
+
type NotificationRow = {
|
|
30
|
+
id: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
label: string;
|
|
33
|
+
description: string;
|
|
34
|
+
presentation: NotificationDefinitionPresentationCatalog | null;
|
|
35
|
+
recipient_kind: "user" | "email";
|
|
36
|
+
required_channels: string[];
|
|
37
|
+
recommended_channels: string[];
|
|
38
|
+
excluded_channels: string[];
|
|
39
|
+
excluded_reason: string | null;
|
|
40
|
+
total: number;
|
|
41
|
+
};
|
|
42
|
+
type Dependencies = {
|
|
43
|
+
db?: SQL;
|
|
44
|
+
getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
|
|
45
|
+
capabilities?: () => Promise<CapabilityRegistryEntry[]>;
|
|
46
|
+
grantCounts?: typeof appAccessGrantCounts;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Core-owned administration read shared by SSR and HTTP; app access is not administrator authority. */
|
|
50
|
+
export function createAppDetailsService(dependencies: Dependencies = {}) {
|
|
51
|
+
const db = dependencies.db ?? sql;
|
|
52
|
+
return async (appId: string, options: { authority: RequestAuthority; locale?: string }): Promise<AppDetails | null> => {
|
|
53
|
+
if (!canRepairAppAccess(options.authority))
|
|
54
|
+
throw new AppAccessManagementError("FORBIDDEN", "Cloud administration authority is required.");
|
|
55
|
+
const locale = options.locale ?? "en";
|
|
56
|
+
const [app, known] = await Promise.all([
|
|
57
|
+
(dependencies.getApp ?? getApp)(appId),
|
|
58
|
+
db<{ known: boolean }[]>`
|
|
59
|
+
SELECT EXISTS (
|
|
60
|
+
SELECT FROM events.app_manifests WHERE app_id = ${appId}
|
|
61
|
+
UNION ALL SELECT FROM events.app_declarations WHERE app_id = ${appId}
|
|
62
|
+
UNION ALL SELECT FROM events.app_subscriptions WHERE app_id = ${appId}
|
|
63
|
+
UNION ALL SELECT FROM notifications.definitions WHERE app_id = ${appId}
|
|
64
|
+
UNION ALL SELECT FROM auth.app_access_state WHERE app_id = ${appId}
|
|
65
|
+
UNION ALL SELECT FROM auth.app_access WHERE app_id = ${appId}
|
|
66
|
+
) AS known
|
|
67
|
+
`,
|
|
68
|
+
]);
|
|
69
|
+
if (!app && !known[0]?.known) return null;
|
|
70
|
+
|
|
71
|
+
const [capabilities, declarations, subscriptions, definitions, grantCounts] = await Promise.all([
|
|
72
|
+
(dependencies.capabilities ?? listCapabilities)(),
|
|
73
|
+
db<(StoredDeclaration & { subscriber_count: number; total: number })[]>`
|
|
74
|
+
SELECT declaration.*, count(*) OVER()::int AS total,
|
|
75
|
+
(SELECT count(*)::int FROM events.app_subscriptions subscription
|
|
76
|
+
WHERE subscription.event_type = declaration.type AND subscription.version = declaration.version
|
|
77
|
+
AND subscription.state <> 'cancelled') AS subscriber_count
|
|
78
|
+
FROM events.app_declarations declaration
|
|
79
|
+
WHERE declaration.app_id = ${appId} AND declaration.retired_at IS NULL
|
|
80
|
+
ORDER BY declaration.type, declaration.version
|
|
81
|
+
LIMIT ${APP_DETAILS_LIST_LIMIT}
|
|
82
|
+
`,
|
|
83
|
+
db<SubscriptionRow[]>`
|
|
84
|
+
SELECT subscription.id, subscription.event_type, subscription.version, subscription.state,
|
|
85
|
+
subscription.reason, subscription.first_seen_at, declaration.app_id AS publisher_app_id,
|
|
86
|
+
subscription.first_seen_at >= now() - ${APP_SUBSCRIPTION_NEW_WINDOW_MS} * interval '1 millisecond' AS is_new,
|
|
87
|
+
declaration.title, count(*) OVER()::int AS total
|
|
88
|
+
FROM events.app_subscriptions subscription
|
|
89
|
+
LEFT JOIN events.app_declarations declaration
|
|
90
|
+
ON declaration.type = subscription.event_type AND declaration.version = subscription.version
|
|
91
|
+
WHERE subscription.app_id = ${appId}
|
|
92
|
+
ORDER BY subscription.first_seen_at DESC, subscription.id
|
|
93
|
+
LIMIT ${APP_DETAILS_LIST_LIMIT}
|
|
94
|
+
`,
|
|
95
|
+
db<NotificationRow[]>`
|
|
96
|
+
SELECT id, kind, label, description, presentation, recipient_kind,
|
|
97
|
+
required_channels, recommended_channels, excluded_channels, excluded_reason, count(*) OVER()::int AS total
|
|
98
|
+
FROM notifications.definitions
|
|
99
|
+
WHERE app_id = ${appId} AND active = true
|
|
100
|
+
ORDER BY id
|
|
101
|
+
LIMIT ${APP_DETAILS_LIST_LIMIT}
|
|
102
|
+
`,
|
|
103
|
+
app?.access?.kind === "grants"
|
|
104
|
+
? (dependencies.grantCounts ?? appAccessGrantCounts)(options.authority, [app])
|
|
105
|
+
: Promise.resolve(new Map<string, number>()),
|
|
106
|
+
]);
|
|
107
|
+
const presentations = new Map<string, { title: string; description: string }>(
|
|
108
|
+
capabilities.flatMap((entry) =>
|
|
109
|
+
(resolveCapabilityManifestPresentation(entry.manifest, entry.presentation, locale).events ?? []).map(
|
|
110
|
+
(event) => [`${entry.appId}.${event.localId}@${event.version}`, event] as const,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
const subscriptionIds = subscriptions.map((subscription) => subscription.id);
|
|
115
|
+
const [deliveryCounts, failedDeliveries] = subscriptionIds.length
|
|
116
|
+
? await Promise.all([
|
|
117
|
+
db<{ subscription_id: string; state: keyof AppDetails["hears"][number]["counts"]; count: number }[]>`
|
|
118
|
+
SELECT subscription_id, state, count(*)::int AS count
|
|
119
|
+
FROM events.app_deliveries
|
|
120
|
+
WHERE subscription_id IN ${db(subscriptionIds)}
|
|
121
|
+
GROUP BY subscription_id, state
|
|
122
|
+
`,
|
|
123
|
+
db<
|
|
124
|
+
{
|
|
125
|
+
subscription_id: string;
|
|
126
|
+
id: string;
|
|
127
|
+
eventId: string;
|
|
128
|
+
code: string | null;
|
|
129
|
+
lastError: string | null;
|
|
130
|
+
attempts: number;
|
|
131
|
+
lastAttemptAt: Date | null;
|
|
132
|
+
}[]
|
|
133
|
+
>`
|
|
134
|
+
SELECT subscription.id AS subscription_id, failed.*
|
|
135
|
+
FROM events.app_subscriptions subscription
|
|
136
|
+
CROSS JOIN LATERAL (
|
|
137
|
+
SELECT id, event_id AS "eventId", code, left(last_error, 1000) AS "lastError",
|
|
138
|
+
attempts, last_attempt_at AS "lastAttemptAt"
|
|
139
|
+
FROM events.app_deliveries
|
|
140
|
+
WHERE subscription_id = subscription.id AND state = 'failed'
|
|
141
|
+
ORDER BY last_attempt_at DESC NULLS LAST, seq DESC
|
|
142
|
+
LIMIT ${APP_DETAILS_FAILED_LIMIT}
|
|
143
|
+
) failed
|
|
144
|
+
WHERE subscription.id IN ${db(subscriptionIds)}
|
|
145
|
+
ORDER BY subscription.id, failed."lastAttemptAt" DESC NULLS LAST, failed.id
|
|
146
|
+
`,
|
|
147
|
+
])
|
|
148
|
+
: [[], []];
|
|
149
|
+
const hears = subscriptions.map((subscription): AppDetails["hears"][number] => {
|
|
150
|
+
const counts: AppDetails["hears"][number]["counts"] = { pending: 0, retrying: 0, done: 0, failed: 0, cancelled: 0 };
|
|
151
|
+
for (const row of deliveryCounts) if (row.subscription_id === subscription.id) counts[row.state] = row.count;
|
|
152
|
+
return {
|
|
153
|
+
subscriptionId: subscription.id,
|
|
154
|
+
eventType: subscription.event_type,
|
|
155
|
+
version: subscription.version,
|
|
156
|
+
publisherAppId: subscription.publisher_app_id ?? subscription.event_type.split(".")[0] ?? subscription.event_type,
|
|
157
|
+
title: presentations.get(`${subscription.event_type}@${subscription.version}`)?.title ?? subscription.title,
|
|
158
|
+
state: subscription.state,
|
|
159
|
+
reason: subscription.reason,
|
|
160
|
+
firstSeenAt: subscription.first_seen_at.toISOString(),
|
|
161
|
+
isNew: subscription.is_new,
|
|
162
|
+
counts,
|
|
163
|
+
failedDeliveries: failedDeliveries
|
|
164
|
+
.filter((row) => row.subscription_id === subscription.id)
|
|
165
|
+
.map(({ subscription_id: _subscriptionId, lastAttemptAt, ...row }) => ({
|
|
166
|
+
...row,
|
|
167
|
+
lastAttemptAt: lastAttemptAt?.toISOString() ?? null,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
app: {
|
|
173
|
+
id: appId,
|
|
174
|
+
...(app ? resolveAppIdentityPresentation(app, locale) : { name: appId, description: null }),
|
|
175
|
+
icon: app?.icon ?? null,
|
|
176
|
+
accent: app?.appearance?.accent ?? null,
|
|
177
|
+
online: app !== null,
|
|
178
|
+
},
|
|
179
|
+
access: {
|
|
180
|
+
kind: app ? (app.access?.kind ?? "legacy") : null,
|
|
181
|
+
roles: app?.access?.kind === "roles" ? [...app.access.roles] : [],
|
|
182
|
+
grantCount: app?.access?.kind === "grants" ? (grantCounts.get(appId) ?? 0) : null,
|
|
183
|
+
platformPermissions: [...(app?.platformPermissions ?? [])],
|
|
184
|
+
},
|
|
185
|
+
sends: declarations.map((declaration) => {
|
|
186
|
+
const presentation = presentations.get(`${declaration.type}@${declaration.version}`);
|
|
187
|
+
return {
|
|
188
|
+
type: declaration.type,
|
|
189
|
+
version: declaration.version,
|
|
190
|
+
title: presentation?.title ?? declaration.title,
|
|
191
|
+
description: presentation?.description ?? declaration.description,
|
|
192
|
+
subject: `${declaration.app_id}.${declaration.subject}`,
|
|
193
|
+
place: declaration.place ? `${declaration.app_id}.${declaration.place}` : null,
|
|
194
|
+
automation: declaration.automation,
|
|
195
|
+
subscriberCount: declaration.subscriber_count,
|
|
196
|
+
};
|
|
197
|
+
}),
|
|
198
|
+
hears,
|
|
199
|
+
notifications: definitions.map((definition) => {
|
|
200
|
+
const presentation = resolveNotificationDefinitionPresentation(
|
|
201
|
+
{ ...definition, excludedReason: definition.excluded_reason ?? undefined },
|
|
202
|
+
locale,
|
|
203
|
+
);
|
|
204
|
+
return {
|
|
205
|
+
id: definition.id,
|
|
206
|
+
kind: definition.kind,
|
|
207
|
+
...presentation,
|
|
208
|
+
recipientKind: definition.recipient_kind,
|
|
209
|
+
requiredChannels: definition.required_channels,
|
|
210
|
+
recommendedChannels: definition.recommended_channels,
|
|
211
|
+
excludedChannels: definition.excluded_channels,
|
|
212
|
+
excludedReason: presentation.excludedReason ?? null,
|
|
213
|
+
};
|
|
214
|
+
}),
|
|
215
|
+
totals: {
|
|
216
|
+
sends: declarations[0]?.total ?? 0,
|
|
217
|
+
hears: subscriptions[0]?.total ?? 0,
|
|
218
|
+
notifications: definitions[0]?.total ?? 0,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const appDetails = createAppDetailsService();
|
|
@@ -18,10 +18,11 @@ export const discoverableAppEntries = async <T extends { appId: string }>(
|
|
|
18
18
|
authority: RequestAuthority | null,
|
|
19
19
|
entries: readonly T[],
|
|
20
20
|
registry: () => Promise<AppRegistryEntry[]> = listApps,
|
|
21
|
+
options: { signal?: AbortSignal } = {},
|
|
21
22
|
): Promise<T[]> => {
|
|
22
23
|
if (!entries.length) return [];
|
|
23
24
|
const ids = new Set(entries.map((entry) => entry.appId));
|
|
24
25
|
const apps = (await registry()).filter((app) => ids.has(app.id));
|
|
25
|
-
const usable = await usableAppIds(authority, apps);
|
|
26
|
+
const usable = await usableAppIds(authority, apps, options);
|
|
26
27
|
return entries.filter((entry) => usable.has(entry.appId));
|
|
27
28
|
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
type Schema = Record<string, unknown>;
|
|
5
|
+
const object = (value: unknown): Schema | null =>
|
|
6
|
+
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Schema) : null;
|
|
7
|
+
const valid = (schema: Schema, value: unknown) => z.fromJSONSchema(schema).safeParse(value).success;
|
|
8
|
+
const UUID = "00000000-0000-4000-8000-000000000001";
|
|
9
|
+
|
|
10
|
+
export type RecordedAppEventShape =
|
|
11
|
+
| { keys: Record<string, RecordedAppEventShape> }
|
|
12
|
+
| { length: number; items: RecordedAppEventShape[] }
|
|
13
|
+
| null;
|
|
14
|
+
|
|
15
|
+
/** Retains key presence and bucketed array structure, without scalar values. */
|
|
16
|
+
export function recordedAppEventShape(data: unknown): RecordedAppEventShape {
|
|
17
|
+
if (Array.isArray(data)) {
|
|
18
|
+
const length = data.length === 0 ? 0 : data.length === 1 ? 1 : data.length <= 4 ? 2 : data.length <= 16 ? 5 : 17;
|
|
19
|
+
const items = [
|
|
20
|
+
...new Map(
|
|
21
|
+
data.map((item) => {
|
|
22
|
+
const shape = recordedAppEventShape(item);
|
|
23
|
+
return [JSON.stringify(shape), shape] as const;
|
|
24
|
+
}),
|
|
25
|
+
).entries(),
|
|
26
|
+
]
|
|
27
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
28
|
+
.map(([, shape]) => shape);
|
|
29
|
+
return { length, items };
|
|
30
|
+
}
|
|
31
|
+
const properties = object(data);
|
|
32
|
+
return properties
|
|
33
|
+
? {
|
|
34
|
+
keys: Object.fromEntries(
|
|
35
|
+
Object.keys(properties)
|
|
36
|
+
.sort()
|
|
37
|
+
.map((key) => [key, recordedAppEventShape(properties[key])]),
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const appEventShapeId = (type: string, version: number, shape: RecordedAppEventShape) => {
|
|
44
|
+
const hash = createHash("sha256")
|
|
45
|
+
.update(JSON.stringify([type, version, shape]))
|
|
46
|
+
.digest("hex");
|
|
47
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Synthesizes values from the declaration, using only recorded structure when supplied. */
|
|
51
|
+
export const appEventShape = (schema: Schema, depth = 0, shape?: RecordedAppEventShape): unknown => {
|
|
52
|
+
if (depth > 16) throw new Error("shape_unavailable");
|
|
53
|
+
if ("const" in schema) return schema.const;
|
|
54
|
+
if (Array.isArray(schema.enum)) return schema.enum[0];
|
|
55
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
56
|
+
if (Array.isArray(schema[key]))
|
|
57
|
+
for (const branch of schema[key]) {
|
|
58
|
+
const sub = object(branch);
|
|
59
|
+
if (!sub) continue;
|
|
60
|
+
try {
|
|
61
|
+
const value = appEventShape(sub, depth + 1, shape);
|
|
62
|
+
if (valid(schema, value)) return value;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(schema.type))
|
|
67
|
+
for (const type of schema.type) {
|
|
68
|
+
try {
|
|
69
|
+
const value = appEventShape({ ...schema, type }, depth + 1, shape);
|
|
70
|
+
if (valid(schema, value)) return value;
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
if (schema.type === "null") return null;
|
|
74
|
+
if (schema.type === "boolean") return false;
|
|
75
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
76
|
+
const step = typeof schema.multipleOf === "number" ? schema.multipleOf : 1;
|
|
77
|
+
let n =
|
|
78
|
+
typeof schema.minimum === "number"
|
|
79
|
+
? schema.minimum
|
|
80
|
+
: typeof schema.exclusiveMinimum === "number"
|
|
81
|
+
? schema.exclusiveMinimum + step
|
|
82
|
+
: 0;
|
|
83
|
+
if (typeof schema.maximum === "number") n = Math.min(n, schema.maximum);
|
|
84
|
+
if (typeof schema.exclusiveMaximum === "number") n = Math.min(n, schema.exclusiveMaximum - step);
|
|
85
|
+
n = Math.ceil(n / step) * step;
|
|
86
|
+
if (valid(schema, n)) return n;
|
|
87
|
+
}
|
|
88
|
+
if (schema.type === "string") {
|
|
89
|
+
const min = typeof schema.minLength === "number" ? schema.minLength : 0;
|
|
90
|
+
const max = typeof schema.maxLength === "number" ? schema.maxLength : 256;
|
|
91
|
+
if (min > 4096) throw new Error("shape_unavailable");
|
|
92
|
+
const exact = typeof schema.pattern === "string" ? schema.pattern.match(/^\^([a-zA-Z0-9_.@:/ -]+)\$$/)?.[1] : undefined;
|
|
93
|
+
const candidates = [
|
|
94
|
+
exact,
|
|
95
|
+
UUID,
|
|
96
|
+
"2000-01-01T00:00:00.000Z",
|
|
97
|
+
"2000-01-01",
|
|
98
|
+
"00:00:00",
|
|
99
|
+
"https://example.invalid/",
|
|
100
|
+
"fixture@example.invalid",
|
|
101
|
+
"127.0.0.1",
|
|
102
|
+
"fixture",
|
|
103
|
+
"local",
|
|
104
|
+
"ipa",
|
|
105
|
+
"a".repeat(Math.max(1, min)),
|
|
106
|
+
"0".repeat(Math.max(1, min)),
|
|
107
|
+
"",
|
|
108
|
+
];
|
|
109
|
+
for (const value of candidates) if (typeof value === "string" && value.length <= max && valid(schema, value)) return value;
|
|
110
|
+
}
|
|
111
|
+
if (schema.type === "array") {
|
|
112
|
+
const minimum = typeof schema.minItems === "number" ? schema.minItems : 0;
|
|
113
|
+
const count = Math.max(minimum, shape && "length" in shape ? shape.length : 0);
|
|
114
|
+
if (count > 100) throw new Error("shape_unavailable");
|
|
115
|
+
const item = object(schema.items);
|
|
116
|
+
const value = Array.from({ length: count }, (_, index) =>
|
|
117
|
+
appEventShape(
|
|
118
|
+
object(Array.isArray(schema.prefixItems) ? schema.prefixItems[index] : null) ?? item ?? { type: "null" },
|
|
119
|
+
depth + 1,
|
|
120
|
+
shape && "items" in shape ? shape.items[index % shape.items.length] : undefined,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
if (valid(schema, value)) return value;
|
|
124
|
+
}
|
|
125
|
+
if (schema.type === "object") {
|
|
126
|
+
const properties = object(schema.properties) ?? {};
|
|
127
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
128
|
+
const value: Record<string, unknown> = {};
|
|
129
|
+
const keys = shape && "keys" in shape ? [...new Set([...required, ...Object.keys(shape.keys)])] : required;
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
if (typeof key !== "string") throw new Error("shape_unavailable");
|
|
132
|
+
const property = object(properties[key]);
|
|
133
|
+
if (!property) throw new Error("shape_unavailable");
|
|
134
|
+
value[key] = appEventShape(property, depth + 1, shape && "keys" in shape ? shape.keys[key] : undefined);
|
|
135
|
+
}
|
|
136
|
+
if (valid(schema, value)) return value;
|
|
137
|
+
}
|
|
138
|
+
throw new Error("shape_unavailable");
|
|
139
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { assertAppEventSize } from "../../_internal/app-event-schemas";
|
|
4
|
+
import { AppEventEnvelopeSchema } from "../../contracts/app-events";
|
|
5
|
+
import { logger } from "../logging";
|
|
6
|
+
import type { StoredDeclaration } from "./declarations";
|
|
7
|
+
|
|
8
|
+
const log = logger("app-events:accept");
|
|
9
|
+
// Additive envelope metadata from a newer publisher is compatible with older Core.
|
|
10
|
+
const Envelope = AppEventEnvelopeSchema.strip().extend({ version: z.number().int().min(1).max(2_147_483_647) });
|
|
11
|
+
const Metadata = z.object({
|
|
12
|
+
id: z.uuid(),
|
|
13
|
+
type: Envelope.shape.type.catch("unknown"),
|
|
14
|
+
source: Envelope.shape.source.catch("unknown"),
|
|
15
|
+
version: Envelope.shape.version.catch(1),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
|
|
19
|
+
const markSeen = async (sequence: number, tx: SQL = db) => {
|
|
20
|
+
// Keep holes until replay or the periodic DLQ/retention check accounts for them.
|
|
21
|
+
// Advancing the high watermark alone would hide eviction before the next tick.
|
|
22
|
+
await tx`
|
|
23
|
+
UPDATE events.app_broker_status SET
|
|
24
|
+
unseen_sequences = (unseen_sequences + CASE WHEN last_sequence + 1 < ${sequence} THEN
|
|
25
|
+
int8multirange(int8range(last_sequence + 1, ${sequence}::bigint, '[)')) ELSE '{}'::int8multirange END)
|
|
26
|
+
- int8multirange(int8range(${sequence}::bigint, ${sequence + 1}::bigint, '[)')),
|
|
27
|
+
last_sequence = GREATEST(last_sequence, ${sequence}), updated_at = now()
|
|
28
|
+
WHERE id = true AND (last_sequence < ${sequence} OR unseen_sequences @> ${sequence}::bigint)
|
|
29
|
+
`;
|
|
30
|
+
};
|
|
31
|
+
const reject = async (input: unknown, sequence: number, code: string) => {
|
|
32
|
+
const metadata = Metadata.safeParse(input);
|
|
33
|
+
if (!metadata.success) {
|
|
34
|
+
log.warn("Discarding invalid app event without a usable ID", { sequence, code });
|
|
35
|
+
await markSeen(sequence);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const { id, type, source, version } = metadata.data;
|
|
39
|
+
await db.begin(async (tx) => {
|
|
40
|
+
// Invalid or oversized input is represented by metadata only, never a second failing payload insert.
|
|
41
|
+
await tx`
|
|
42
|
+
INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
|
|
43
|
+
VALUES (${id}::uuid, ${type}, ${version}, ${source}, ${`unknown:${id}`}, '{}'::jsonb, 'rejected', ${code})
|
|
44
|
+
ON CONFLICT(id) DO NOTHING
|
|
45
|
+
`;
|
|
46
|
+
await markSeen(sequence, tx);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const accept = async (input: unknown, sequence: number, attempt = 3) => {
|
|
50
|
+
const parsed = Envelope.safeParse(input);
|
|
51
|
+
if (!parsed.success) return reject(input, sequence, "invalid_envelope");
|
|
52
|
+
const envelope = parsed.data;
|
|
53
|
+
try {
|
|
54
|
+
assertAppEventSize(envelope);
|
|
55
|
+
} catch {
|
|
56
|
+
return reject(envelope, sequence, "event_too_large");
|
|
57
|
+
}
|
|
58
|
+
// Persisted declarations remain authoritative while discovery is unavailable.
|
|
59
|
+
try {
|
|
60
|
+
await reconcile();
|
|
61
|
+
} catch {
|
|
62
|
+
log.warn("Declaration refresh failed; accepting against persisted declarations", { sequence });
|
|
63
|
+
}
|
|
64
|
+
const [declaration] = await db<StoredDeclaration[]>`
|
|
65
|
+
SELECT * FROM events.app_declarations
|
|
66
|
+
WHERE app_id = ${envelope.source} AND type = ${envelope.type} AND version = ${envelope.version}
|
|
67
|
+
`;
|
|
68
|
+
let code: string | null = null;
|
|
69
|
+
if (!declaration) {
|
|
70
|
+
if (attempt < 3) throw new Error("Event declaration registration is not ready");
|
|
71
|
+
code = "event_not_declared";
|
|
72
|
+
} else if (declaration.retired_at) {
|
|
73
|
+
code = "event_not_declared";
|
|
74
|
+
} else if (
|
|
75
|
+
envelope.subject.type !== `${declaration.app_id}.${declaration.subject}` ||
|
|
76
|
+
(declaration.place ? envelope.place?.type !== `${declaration.app_id}.${declaration.place}` : envelope.place !== undefined)
|
|
77
|
+
) {
|
|
78
|
+
code = "reference_mismatch";
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
if (!z.fromJSONSchema(declaration.data_schema).safeParse(envelope.data).success) code = "schema_mismatch";
|
|
82
|
+
} catch {
|
|
83
|
+
code = "invalid_declaration_schema";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const subjectKey = `${envelope.subject.type}:${envelope.subject.id}`;
|
|
87
|
+
try {
|
|
88
|
+
await db.begin(async (tx) => {
|
|
89
|
+
const inserted = await tx`
|
|
90
|
+
INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
|
|
91
|
+
VALUES (${envelope.id}::uuid, ${envelope.type}, ${envelope.version}, ${envelope.source}, ${subjectKey},
|
|
92
|
+
${JSON.stringify(envelope)}::text::jsonb, ${code ? "rejected" : "accepted"}, ${code})
|
|
93
|
+
ON CONFLICT(id) DO NOTHING RETURNING id
|
|
94
|
+
`;
|
|
95
|
+
if (inserted.length && !code) {
|
|
96
|
+
await tx`
|
|
97
|
+
INSERT INTO events.app_deliveries(id, subscription_id, event_id, subject_key)
|
|
98
|
+
SELECT gen_random_uuid(), id, ${envelope.id}::uuid, ${subjectKey} FROM events.app_subscriptions
|
|
99
|
+
WHERE event_type = ${envelope.type} AND version = ${envelope.version} AND state = 'active'
|
|
100
|
+
ON CONFLICT(subscription_id, event_id) DO NOTHING
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
await markSeen(sequence, tx);
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// PostgreSQL data exceptions and CHECK failures are deterministic; infrastructure failures retry.
|
|
107
|
+
const sqlState = error instanceof Error && "errno" in error ? error.errno : undefined;
|
|
108
|
+
if (typeof sqlState === "string" && (sqlState.startsWith("22") || sqlState === "23514")) {
|
|
109
|
+
await reject(envelope, sequence, sqlState === "23514" ? "event_too_large" : "invalid_envelope");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return { accept, markSeen };
|
|
116
|
+
}
|