@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.
Files changed (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. 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
+ }