@k2b/cloud 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/package.json +7 -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/PermissionEditor.tsx +9 -3
  21. package/src/access/PrincipalPicker.tsx +14 -4
  22. package/src/access/ResourceApiKeys.tsx +3 -4
  23. package/src/access/app-access-messages.ts +6 -0
  24. package/src/account/ProfilePictureDialog.tsx +699 -0
  25. package/src/account/profile-picture-messages.ts +269 -0
  26. package/src/account/ui.ts +7 -0
  27. package/src/ai/admin.ts +3 -0
  28. package/src/ai/app-frame.ts +12 -0
  29. package/src/ai/application-tasks.ts +101 -0
  30. package/src/ai/assistant-models.ts +126 -64
  31. package/src/ai/capabilities.ts +19 -4
  32. package/src/ai/chart-block.ts +63 -30
  33. package/src/ai/chat/chart-block.tsx +12 -3
  34. package/src/ai/chat/composer-adapter.ts +8 -7
  35. package/src/ai/chat/message-actions.tsx +1 -1
  36. package/src/ai/chat/message-utils.ts +2 -2
  37. package/src/ai/chat/presentation.tsx +22 -1
  38. package/src/ai/chat-quotas.ts +5 -4
  39. package/src/ai/chat-tasks.ts +2 -2
  40. package/src/ai/client/controller.ts +11 -10
  41. package/src/ai/client/projection.ts +3 -3
  42. package/src/ai/compaction.ts +3 -3
  43. package/src/ai/credentials.ts +13 -114
  44. package/src/ai/default-tool-contracts.ts +4 -3
  45. package/src/ai/dictation-runtime.ts +4 -4
  46. package/src/ai/enrich.ts +2 -2
  47. package/src/ai/executor.ts +31 -22
  48. package/src/ai/http.ts +17 -29
  49. package/src/ai/index.ts +11 -5
  50. package/src/ai/inference-calls.ts +5 -4
  51. package/src/ai/maintenance.ts +1 -1
  52. package/src/ai/memory-learning-runs.ts +5 -5
  53. package/src/ai/memory-learning.ts +3 -3
  54. package/src/ai/message-queue.ts +1 -2
  55. package/src/ai/migrate.ts +75 -42
  56. package/src/ai/model-access-migrate.ts +3 -17
  57. package/src/ai/model-access.ts +44 -85
  58. package/src/ai/model-card.tsx +134 -0
  59. package/src/ai/model-catalog.ts +359 -0
  60. package/src/ai/model-chooser-messages.ts +88 -0
  61. package/src/ai/model-chooser.tsx +577 -0
  62. package/src/ai/model-delete.ts +234 -0
  63. package/src/ai/model-fixture.ts +39 -0
  64. package/src/ai/model-icon.tsx +36 -0
  65. package/src/ai/model-overview.ts +53 -0
  66. package/src/ai/model-usage.ts +45 -0
  67. package/src/ai/openai-api-messages.ts +0 -4
  68. package/src/ai/openai-api.ts +26 -9
  69. package/src/ai/personal-agent.ts +1 -1
  70. package/src/ai/prefs.ts +12 -10
  71. package/src/ai/projects-routes.ts +2 -1
  72. package/src/ai/projects.ts +13 -12
  73. package/src/ai/protocol.ts +2 -2
  74. package/src/ai/provider-connections.ts +161 -0
  75. package/src/ai/provider-errors.ts +55 -6
  76. package/src/ai/provider-fetch.ts +4 -0
  77. package/src/ai/provider.ts +1 -1
  78. package/src/ai/quota-limits.ts +36 -0
  79. package/src/ai/quota-provider.ts +4 -2
  80. package/src/ai/quota-report.ts +7 -4
  81. package/src/ai/quotas.ts +19 -8
  82. package/src/ai/recent-resources.ts +6 -0
  83. package/src/ai/request-headers.ts +14 -87
  84. package/src/ai/resource-refs.ts +6 -2
  85. package/src/ai/routes.ts +17 -18
  86. package/src/ai/runtime.ts +50 -22
  87. package/src/ai/settings.ts +93 -580
  88. package/src/ai/store.ts +45 -23
  89. package/src/ai/stream.ts +1 -1
  90. package/src/ai/structured-runs.ts +2 -2
  91. package/src/ai/structured.ts +21 -44
  92. package/src/ai/task-contracts.ts +25 -5
  93. package/src/ai/task-execution.ts +15 -6
  94. package/src/ai/task-model.ts +9 -0
  95. package/src/ai/transcription.ts +27 -22
  96. package/src/ai/types.ts +50 -62
  97. package/src/ai/ui.tsx +13 -0
  98. package/src/ai/usage.ts +8 -8
  99. package/src/ai/validate.ts +8 -7
  100. package/src/ai/view-block.ts +2 -2
  101. package/src/api/accounts-entities.ts +16 -0
  102. package/src/api/admin-ai-quotas.ts +34 -56
  103. package/src/api/admin-ai-usage.ts +1 -1
  104. package/src/api/admin-ai.ts +173 -0
  105. package/src/api/admin-core-settings.ts +68 -244
  106. package/src/api/app-approval.ts +3 -1
  107. package/src/api/app-event-delivery.ts +51 -0
  108. package/src/api/app-events.ts +123 -0
  109. package/src/api/app-resource-access.ts +48 -0
  110. package/src/api/auth/account-mismatch.ts +24 -0
  111. package/src/api/auth.ts +26 -3
  112. package/src/api/capabilities.ts +28 -4
  113. package/src/api/index.ts +7 -1
  114. package/src/api/mcp.ts +175 -2
  115. package/src/api/me.ts +6 -4
  116. package/src/api/pwa-phone.ts +75 -26
  117. package/src/api/pwa.ts +34 -8
  118. package/src/api/resource-links.ts +83 -6
  119. package/src/api/resource-resolve.ts +50 -0
  120. package/src/api/search/schemas.ts +3 -1
  121. package/src/api/search.ts +9 -7
  122. package/src/app-pwa/contracts.ts +4 -0
  123. package/src/app-pwa/core-query-error.ts +20 -0
  124. package/src/app-pwa/index.ts +1 -0
  125. package/src/app-pwa/service.ts +91 -0
  126. package/src/app-pwa/transport.ts +22 -0
  127. package/src/approvals/messages.ts +2 -2
  128. package/src/browser/cloud-id.ts +86 -0
  129. package/src/browser/resource-clipboard.ts +4 -0
  130. package/src/browser/resource-links.ts +52 -0
  131. package/src/capabilities/server-invoke.ts +2 -11
  132. package/src/capabilities/testing.ts +5 -2
  133. package/src/cli/access.ts +6 -5
  134. package/src/cli/admin/ai-models.ts +199 -0
  135. package/src/cli/admin/ai-quotas.ts +6 -102
  136. package/src/cli/admin/ai-usage.ts +4 -4
  137. package/src/cli/admin/app-credentials.ts +5 -3
  138. package/src/cli/admin/index.ts +5 -2
  139. package/src/cli/apps.ts +89 -26
  140. package/src/cli/capabilities.ts +22 -1
  141. package/src/cli/events.ts +320 -0
  142. package/src/config/env.ts +40 -0
  143. package/src/contracts/app-access.ts +26 -2
  144. package/src/contracts/app-details.ts +89 -0
  145. package/src/contracts/app-events.ts +51 -0
  146. package/src/contracts/app.ts +21 -5
  147. package/src/contracts/capabilities.ts +143 -3
  148. package/src/contracts/index.ts +2 -0
  149. package/src/contracts/notification-types.ts +36 -9
  150. package/src/contracts/pwa.ts +17 -1
  151. package/src/contracts/registry.ts +6 -0
  152. package/src/contracts/resource-clipboard.ts +35 -0
  153. package/src/contracts/resource-pages.ts +140 -0
  154. package/src/contracts/resource-resolution.ts +13 -0
  155. package/src/contracts/shared.ts +4 -0
  156. package/src/contracts/user-notifications.ts +2 -0
  157. package/src/events/app-event-delivery.ts +100 -0
  158. package/src/events/app-events.testing.ts +65 -0
  159. package/src/events/app-events.ts +134 -0
  160. package/src/events/core-user-created.ts +24 -0
  161. package/src/events/index.ts +9 -0
  162. package/src/events/testing.ts +2 -0
  163. package/src/index.ts +1 -1
  164. package/src/server/api/respond.ts +8 -3
  165. package/src/server/index.ts +1 -0
  166. package/src/server/middleware/app-access.ts +1 -1
  167. package/src/server/middleware/auth.ts +21 -7
  168. package/src/server/middleware/index.ts +1 -0
  169. package/src/server/middleware/invocation.ts +2 -0
  170. package/src/server/public-storage.ts +106 -0
  171. package/src/server/services/access.ts +8 -0
  172. package/src/services/accounts/app.ts +6 -0
  173. package/src/services/accounts/entities.ts +14 -3
  174. package/src/services/accounts/groups.ts +3 -1
  175. package/src/services/accounts/identities.ts +62 -10
  176. package/src/services/accounts/users.ts +7 -6
  177. package/src/services/app-access.ts +25 -7
  178. package/src/services/app-details.ts +224 -0
  179. package/src/services/app-discovery.ts +2 -1
  180. package/src/services/app-event-shapes.ts +139 -0
  181. package/src/services/app-events/accept.ts +116 -0
  182. package/src/services/app-events/admin.ts +250 -0
  183. package/src/services/app-events/declarations.ts +187 -0
  184. package/src/services/app-events/deliver.ts +218 -0
  185. package/src/services/app-events.ts +92 -0
  186. package/src/services/config-conflict.ts +11 -0
  187. package/src/services/core-user-events.ts +1 -0
  188. package/src/services/freeipa-config.ts +9 -0
  189. package/src/services/geo/ledger.ts +28 -10
  190. package/src/services/geo/service.ts +43 -24
  191. package/src/services/identity/core-origin.ts +12 -0
  192. package/src/services/identity/invocation-actor.ts +20 -2
  193. package/src/services/identity/invocation-authority.ts +4 -0
  194. package/src/services/identity/invocation-token.ts +238 -56
  195. package/src/services/identity/issue-invocation.ts +6 -1
  196. package/src/services/image-metadata.ts +65 -1
  197. package/src/services/index.ts +13 -1
  198. package/src/services/ipa/sync.ts +6 -2
  199. package/src/services/ipa/users.ts +4 -1
  200. package/src/services/mandates/policy.ts +19 -12
  201. package/src/services/notifications/browser.ts +4 -0
  202. package/src/services/notifications/catalog.ts +5 -2
  203. package/src/services/notifications/platform.ts +7 -2
  204. package/src/services/notifications/user.ts +18 -7
  205. package/src/services/oauth-tokens.ts +28 -2
  206. package/src/services/object-store.ts +217 -0
  207. package/src/services/providers/local/users.ts +5 -0
  208. package/src/services/pwa-devices.ts +147 -37
  209. package/src/services/session/index.ts +14 -0
  210. package/src/services/settings/core-settings.ts +71 -43
  211. package/src/services/settings/index.ts +8 -0
  212. package/src/services/settings/versions.ts +26 -0
  213. package/src/shared/ai-models.ts +274 -0
  214. package/src/shared/ai-quotas.ts +2 -2
  215. package/src/shared/ai-usage.ts +2 -2
  216. package/src/shared/index.ts +2 -0
  217. package/src/shared-text/documents.ts +25 -8
  218. package/src/shared-text/log.ts +1 -1
  219. package/src/shared-text/text-diff.ts +75 -0
  220. package/src/ssr/AdminArea.tsx +131 -0
  221. package/src/ssr/AdminLayout.tsx +9 -3
  222. package/src/ssr/AdminSidebar.tsx +7 -7
  223. package/src/ssr/AppAccessDenied.tsx +2 -0
  224. package/src/ssr/AppBadge.tsx +14 -0
  225. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  226. package/src/ssr/CloudShell.island.tsx +25 -4
  227. package/src/ssr/Layout.tsx +8 -6
  228. package/src/ssr/PwaLayout.tsx +16 -1
  229. package/src/ssr/PwaPartCount.island.tsx +23 -0
  230. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  231. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  232. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  233. package/src/ssr/admin-active-link.ts +16 -0
  234. package/src/ssr/admin-navigation.ts +25 -20
  235. package/src/ssr/app-badges.ts +31 -7
  236. package/src/ssr/index.ts +8 -0
  237. package/src/ssr/page-trail-runtime.ts +3 -1
  238. package/src/ssr/platform-messages.ts +9 -1
  239. package/src/ssr/pwa-messages.ts +2 -2
  240. package/src/ssr/rail-navigation.ts +5 -1
  241. package/src/ssr/settings-draft.ts +153 -0
  242. package/src/ssr/shell-data.tsx +2 -0
  243. package/src/styles/global.css +1 -0
  244. package/src/styles/profile-picture.css +270 -0
  245. package/src/styles/utilities-navigation.css +6 -0
  246. package/src/workflows/ai/policy.ts +49 -0
  247. package/src/workflows/ai/runtime.ts +131 -21
  248. package/src/workflows/ai/store.ts +283 -35
  249. package/src/workflows/ai/types.ts +8 -2
  250. package/src/workflows/ai-actions.ts +148 -120
  251. package/src/workflows/authoring.ts +31 -0
  252. package/src/workflows/contracts.ts +1 -0
  253. package/src/workflows/definition.ts +2 -0
  254. package/src/workflows/editor.ts +15 -0
  255. package/src/workflows/index.ts +2 -1
  256. package/src/workflows/language/compiler.ts +34 -3
  257. package/src/workflows/runtime/executor.ts +132 -33
  258. package/src/workflows/runtime/index.ts +2 -0
  259. package/src/workflows/runtime/ports.ts +5 -1
  260. package/src/workflows/store/actions.ts +1 -1
  261. package/src/workflows/store/definitions.ts +5 -0
  262. package/src/workflows/store/runs.ts +5 -3
  263. package/src/workflows/store/worker.ts +7 -2
  264. package/src/ai/model-pricing.ts +0 -43
  265. package/src/ai/model-request-settings.ts +0 -98
  266. package/src/ai/settings-messages.ts +0 -129
  267. package/src/shared/ai-model-request-settings.ts +0 -21
package/src/api/pwa.ts CHANGED
@@ -28,7 +28,7 @@ const log = logger("pwa");
28
28
 
29
29
  const MESSAGES: Record<PwaError["code"], string> = {
30
30
  UNAVAILABLE: "The mobile app is not available right now.",
31
- REAUTHENTICATE: "Sign in again to pair a phone.",
31
+ REAUTHENTICATE: "Sign in again to pair a device.",
32
32
  FORBIDDEN: "Use Cloud on the web for this.",
33
33
  ACCOUNT_BLOCKED: "This account cannot use the mobile app right now.",
34
34
  INVALID_REQUEST: "The request is not valid.",
@@ -40,7 +40,7 @@ const MESSAGES: Record<PwaError["code"], string> = {
40
40
  ALREADY_PAIRED: "This app is paired with another account.",
41
41
  ACCOUNT_MISMATCH: "This browser is signed in to another account.",
42
42
  LIMIT_REACHED: "The limit for this account was reached.",
43
- UNPAIRED: "This phone is not paired.",
43
+ UNPAIRED: "This device is not paired.",
44
44
  };
45
45
 
46
46
  /** `{ code, message }` with a safe English message; the UI maps codes to its own catalog. */
@@ -95,7 +95,7 @@ const webActor = async (c: Context) => {
95
95
  const session = token ? await auth.session.authenticateRequest(c, token) : null;
96
96
  if (!session) throw new PwaError("REAUTHENTICATE", 403);
97
97
  if (session.data.kind !== "web") throw new PwaError("FORBIDDEN", 403);
98
- return { userId: session.user.id, sid: session.data.sid };
98
+ return { userId: session.user.id, sid: session.data.sid, admin: session.user.roles.includes("admin") };
99
99
  };
100
100
 
101
101
  /** The phone this request also comes from (Chrome on a paired Android phone shares the app's cookies). */
@@ -119,7 +119,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
119
119
  "/pairings",
120
120
  describeRoute({
121
121
  tags,
122
- summary: "Start pairing a phone",
122
+ summary: "Start pairing a device",
123
123
  description: "Needs a web sign-in from the last ten minutes. The link secret is returned once.",
124
124
  responses: { 201: jsonResponse(PwaPairingStartResultSchema, "Pairing started") },
125
125
  }),
@@ -129,13 +129,33 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
129
129
  return c.json(await service.startPairing(actor), 201);
130
130
  },
131
131
  )
132
+ .post(
133
+ "/users/:userId/pairings",
134
+ describeRoute({
135
+ tags,
136
+ summary: "Pair the Cloud app for another person",
137
+ description:
138
+ "Administrator web session only, signed in within ten minutes. App sessions and API keys are refused. The target account must be active. Uses the existing account pairing and device limits. After the administrator confirms the device code, the device must explicitly accept before credentials are issued.",
139
+ responses: {
140
+ 201: jsonResponse(PwaPairingStartResultSchema, "Administrative pairing started"),
141
+ 403: { description: "Administrator, fresh web sign-in or active account required" },
142
+ },
143
+ }),
144
+ v("param", z.object({ userId: z.string().uuid() }), pwaInvalidRequest),
145
+ async (c) => {
146
+ const actor = await webActor(c);
147
+ if (!actor.admin) throw new PwaError("FORBIDDEN", 403);
148
+ if (!shellAvailable(c)) throw new PwaError("UNAVAILABLE", 503);
149
+ return c.json(await service.startUserPairing(actor, c.req.valid("param").userId), 201);
150
+ },
151
+ )
132
152
  .get(
133
153
  "/pairings/:id",
134
154
  describeRoute({
135
155
  tags,
136
156
  summary: "Read a pairing",
137
157
  description:
138
- "Only for the web session that started it. Never returns the code. A pending read is held for up to one poll interval and answers as soon as the phone claims the link.",
158
+ "Only for the web session that started it. Never returns the code. A pending read is held for up to one poll interval and answers as soon as the device claims the link.",
139
159
  responses: { 200: jsonResponse(PwaPairingStatusSchema, "Pairing state") },
140
160
  }),
141
161
  v("param", IdParamSchema, pwaInvalidRequest),
@@ -147,7 +167,13 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
147
167
  )
148
168
  .post(
149
169
  "/pairings/:id/confirm",
150
- describeRoute({ tags, summary: "Confirm a pairing with the code the phone shows", responses: { 204: { description: "Confirmed" } } }),
170
+ describeRoute({
171
+ tags,
172
+ summary: "Confirm a pairing with the code the device shows",
173
+ description:
174
+ "Administrative pairings enter awaiting_device; only the claiming device can then accept or reject. Self-pairing proceeds as before.",
175
+ responses: { 204: { description: "Confirmed" } },
176
+ }),
151
177
  v("param", IdParamSchema, pwaInvalidRequest),
152
178
  v("json", PwaPairingConfirmSchema, pwaInvalidRequest),
153
179
  async (c) => {
@@ -166,7 +192,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
166
192
  )
167
193
  .get(
168
194
  "/devices",
169
- describeRoute({ tags, summary: "List paired phones", responses: { 200: jsonResponse(PwaDeviceListSchema, "Active phones") } }),
195
+ describeRoute({ tags, summary: "List paired devices", responses: { 200: jsonResponse(PwaDeviceListSchema, "Active devices") } }),
170
196
  async (c) => {
171
197
  const actor = await webActor(c);
172
198
  return c.json({ items: await service.list(actor, await currentDeviceId(c, actor.userId)) });
@@ -174,7 +200,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
174
200
  )
175
201
  .delete(
176
202
  "/devices/:id",
177
- describeRoute({ tags, summary: "Remove a paired phone", responses: { 204: { description: "Removed; idempotent" } } }),
203
+ describeRoute({ tags, summary: "Remove a paired device", responses: { 204: { description: "Removed; idempotent" } } }),
178
204
  v("param", IdParamSchema, pwaInvalidRequest),
179
205
  async (c) => {
180
206
  await service.revoke(await webActor(c), c.req.valid("param").id);
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
- import { getCapability } from "../_internal/registry";
2
+ import { canonicalRoute } from "../_internal/app-path";
3
+ import { readBoundedJson } from "../_internal/bounded-json";
4
+ import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
5
+ import { getCapability, listApps, listCapabilities } from "../_internal/registry";
3
6
  import {
7
+ CAPABILITY_MAX_RESULT_BYTES,
4
8
  CapabilitySemanticLinkSchema,
5
9
  type CloudResourceRef,
6
10
  CloudResourceRefSchema,
@@ -8,20 +12,26 @@ import {
8
12
  cloudResourceRefAppId,
9
13
  resolveCapabilityResourceReader,
10
14
  } from "../contracts/capabilities";
15
+ import { parseCloudResourceUrl } from "../contracts/resource-clipboard";
16
+ import { localCloudResourceUrl, matchCloudResourcePage } from "../contracts/resource-pages";
17
+ import type { ResolvedCloudResourceUrl } from "../contracts/resource-resolution";
11
18
  import type { RequestAuthority } from "../server";
19
+ import * as settings from "../services/settings";
20
+ import { publicCloudOrigin } from "../shared/app-url";
12
21
  import { dispatchCapability } from "./capabilities";
13
22
 
14
- export const resolveCloudResourceHref = async (
23
+ const readCloudResourceLink = async (
15
24
  params: { ref: CloudResourceRef; request: Request; authority: RequestAuthority; locale?: string },
16
25
  dependencies: { getCapability?: typeof getCapability; dispatch?: typeof dispatchCapability } = {},
17
- ): Promise<string | null> => {
26
+ ): Promise<ResolvedCloudResourceUrl | null> => {
18
27
  const parsed = CloudResourceRefSchema.safeParse(params.ref);
19
28
  if (!parsed.success) return null;
20
29
  const ref = parsed.data;
21
30
  const appId = cloudResourceRefAppId(ref);
22
31
  const app = await (dependencies.getCapability ?? getCapability)(appId);
23
32
  if (!app || app.appId !== appId) return null;
24
- const reader = resolveCapabilityResourceReader(app.manifest, ref);
33
+ const manifest = params.locale ? resolveCapabilityManifestPresentation(app.manifest, app.presentation, params.locale) : app.manifest;
34
+ const reader = resolveCapabilityResourceReader(manifest, ref);
25
35
  if (!reader) return null;
26
36
  const response = await (dependencies.dispatch ?? dispatchCapability)({
27
37
  request: params.request,
@@ -34,10 +44,77 @@ export const resolveCloudResourceHref = async (
34
44
  input: { id: ref.id },
35
45
  });
36
46
  if (!response.ok) return null;
37
- const result = capabilityResultSchema(z.unknown(), { consumer: true }).safeParse(await response.json());
47
+ const body = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES, params.request.signal);
48
+ if (!body.ok) return null;
49
+ const result = capabilityResultSchema(z.unknown(), { consumer: true }).safeParse(body.data);
38
50
  if (!result.success) return null;
39
51
  const link = CapabilitySemanticLinkSchema.safeParse(
40
52
  result.data.links?.find((link) => link.rel === "open") ?? result.data.links?.find((link) => link.rel === "edit"),
41
53
  );
42
- return link.success ? link.data.href : null;
54
+ if (!link.success) return null;
55
+ const type = manifest.types.find((type) => `${appId}.${type.localId}` === ref.type);
56
+ if (!type) return null;
57
+ const reference = result.data.refs?.find((entry) => entry.type === ref.type && entry.id === ref.id);
58
+ const icon = reference?.icon ?? type.icon;
59
+ return { ref, title: reference?.title ?? type.title, ...(icon ? { icon } : {}), href: link.data.href };
60
+ };
61
+
62
+ /** Resolve a known reference through its authorized reader. */
63
+ export const resolveCloudResourceHref = async (
64
+ params: Parameters<typeof readCloudResourceLink>[0],
65
+ dependencies: Parameters<typeof readCloudResourceLink>[1] = {},
66
+ ): Promise<string | null> => (await readCloudResourceLink(params, dependencies))?.href ?? null;
67
+
68
+ /** Recognize one local URL, then read the resource under the caller's authority. All failures are opaque. */
69
+ export const resolveCloudResourceUrl = async (
70
+ params: { url: string; request: Request; authority: RequestAuthority; locale?: string },
71
+ dependencies: Parameters<typeof readCloudResourceLink>[1] & {
72
+ appUrl?: () => Promise<string | null | undefined>;
73
+ listApps?: typeof listApps;
74
+ listCapabilities?: typeof listCapabilities;
75
+ } = {},
76
+ ): Promise<ResolvedCloudResourceUrl | null> => {
77
+ try {
78
+ const configured = await (dependencies.appUrl ?? (() => settings.get<string>("app.url")))();
79
+ const origin = configured ? publicCloudOrigin(configured) : null;
80
+ const url = localCloudResourceUrl(params.url, origin);
81
+ if (!url) return null;
82
+ // Permalinks share the existing parser; fragments and incidental query parameters do not change identity.
83
+ let ref = parseCloudResourceUrl(`${url.pathname}`, origin ?? url.origin);
84
+ if (!ref && !url.pathname.startsWith("/open/")) {
85
+ const apps = await (dependencies.listApps ?? listApps)();
86
+ // Honor a more specific live owner, including apps that publish no resource pages.
87
+ // A root or ancestor route must not capture their links. Equal prefix claims fail closed.
88
+ const pathname = canonicalRoute(url.pathname);
89
+ const namespace = pathname.split("/");
90
+ const routes = apps.flatMap((app) =>
91
+ app.routes.flatMap((route) => {
92
+ const prefix = canonicalRoute(route);
93
+ if (!prefix.startsWith("/") || /[%\\?#\s]/.test(prefix) || prefix.split("/").some((part) => part === "." || part === ".."))
94
+ return [];
95
+ if (namespace[1] === "app" && namespace[2] && namespace[2] !== app.id) return [];
96
+ return prefix === "/" || pathname === prefix || pathname.startsWith(`${prefix}/`)
97
+ ? [{ appId: app.id, length: prefix.length }]
98
+ : [];
99
+ }),
100
+ );
101
+ const longest = routes.reduce((length, route) => Math.max(length, route.length), 0);
102
+ const owners = new Set(routes.filter((route) => route.length === longest).map((route) => route.appId));
103
+ if (owners.size !== 1) return null;
104
+ const catalog = await (dependencies.listCapabilities ?? listCapabilities)(apps);
105
+ const pages = catalog.flatMap((entry) => {
106
+ const app = apps.find((app) => app.id === entry.appId);
107
+ if (!app || !owners.has(app.id) || entry.manifest.appId !== app.id) return [];
108
+ return (entry.manifest.resourcePages ?? []).flatMap((page) => {
109
+ const ref = { type: `${app.id}.${page.type}`, id: "candidate" };
110
+ return resolveCapabilityResourceReader(entry.manifest, ref) ? [{ type: ref.type, path: page.path, prefixes: app.routes }] : [];
111
+ });
112
+ });
113
+ ref = matchCloudResourcePage(`${url.pathname}${url.search}`, pages);
114
+ }
115
+ return ref ? await readCloudResourceLink({ ...params, ref }, dependencies) : null;
116
+ } catch {
117
+ // Never include a pasted URL, resource existence, or provider error in this result.
118
+ return null;
119
+ }
43
120
  };
@@ -0,0 +1,50 @@
1
+ import { Hono } from "hono";
2
+ import { describeRoute, resolver } from "hono-openapi";
3
+ import { z } from "zod";
4
+ import { readBoundedJson } from "../_internal/bounded-json";
5
+ import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
6
+ import { RESOURCE_URL_MAX_LENGTH } from "../contracts/resource-pages";
7
+ import { ResolvedCloudResourceUrlSchema } from "../contracts/resource-resolution";
8
+ import { type AuthContext, auth, getLocale } from "../server";
9
+ import { resolveCloudResourceUrl } from "./resource-links";
10
+
11
+ const Input = z.object({ url: z.string().min(1).max(RESOURCE_URL_MAX_LENGTH) }).strict();
12
+ const NotFound = { error: "Resource not found" } as const;
13
+
14
+ export const createResourceResolveRoutes = (resolve = resolveCloudResourceUrl) =>
15
+ new Hono<AuthContext>().post(
16
+ "/resolve",
17
+ auth.readOnly,
18
+ auth.requireRole("authenticated"),
19
+ describeRoute({
20
+ tags: ["Resources"],
21
+ summary: "Resolve a Cloud URL through its authorized resource reader",
22
+ requestBody: { required: true, content: { "application/json": { schema: resolver(Input) } } },
23
+ responses: {
24
+ 200: { description: "Readable resource", content: { "application/json": { schema: resolver(ResolvedCloudResourceUrlSchema) } } },
25
+ 404: {
26
+ description: "Resource not found",
27
+ content: { "application/json": { schema: resolver(z.object({ error: z.literal(NotFound.error) })) } },
28
+ },
29
+ },
30
+ }),
31
+ async (c) => {
32
+ try {
33
+ // JSON may encode each UTF-16 unit as six bytes. Bound the envelope before parsing.
34
+ const body = await readBoundedJson(c.req.raw, RESOURCE_URL_MAX_LENGTH * 6 + 100, c.req.raw.signal);
35
+ const input = body.ok ? Input.safeParse(body.data) : null;
36
+ if (!input?.success) return c.json(NotFound, 404);
37
+ const result = await withCapabilityRequestTimeout(c, "query", () =>
38
+ resolve({
39
+ url: input.data.url,
40
+ request: c.req.raw,
41
+ authority: auth.getAuthority(c),
42
+ locale: getLocale(c),
43
+ }),
44
+ );
45
+ return result ? c.json(result, 200) : c.json(NotFound, 404);
46
+ } catch {
47
+ return c.json(NotFound, 404);
48
+ }
49
+ },
50
+ );
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { CloudResourceRefSchema } from "../../contracts";
3
- import { CapabilitySearchTagManifestSchema } from "../../contracts/capabilities";
3
+ import { CapabilitySearchTagManifestSchema, CloudResourceReferenceSchema } from "../../contracts/capabilities";
4
4
 
5
5
  const TAG_PATTERN = /^[^\s#]+$/;
6
6
  const SEARCH_BASE_URL = "https://cloud.invalid";
@@ -69,6 +69,8 @@ export const SearchItemSchema = z.object({
69
69
  ref: CloudResourceRefSchema,
70
70
  title: z.string(),
71
71
  href: SameOriginPathSchema,
72
+ context: CloudResourceReferenceSchema.shape.context,
73
+ status: CloudResourceReferenceSchema.shape.status,
72
74
  preview: z.string().optional(),
73
75
  icon: z.string().optional(),
74
76
  priority: z.number().int().min(0).max(9).optional(),
package/src/api/search.ts CHANGED
@@ -139,9 +139,14 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
139
139
  const stream = acceptsStream(c.req.header("accept"));
140
140
  const requestId = normalizeInvocationRequestId(c.req.header("x-request-id"));
141
141
  const invocationAuthority = invocationAuthorityFromRequest(auth.getAuthority(c));
142
+ // One request-wide budget covers resource access, signing and provider I/O.
143
+ const providerDeadline = AbortSignal.timeout(PROVIDER_TIMEOUT_MS);
144
+ // A client that stops reading a stream ends the search too.
145
+ const streamCancelled = new AbortController();
146
+ const fanoutSignal = AbortSignal.any([c.req.raw.signal, providerDeadline, streamCancelled.signal]);
142
147
  let entries: CapabilityRegistryEntry[];
143
148
  try {
144
- entries = await discoverableAppEntries(appAccessAuthority(c), await registry(), dependencies.listApps);
149
+ entries = await discoverableAppEntries(appAccessAuthority(c), await registry(), dependencies.listApps, { signal: fanoutSignal });
145
150
  } catch (error) {
146
151
  log.warn("Search capability registry unavailable", { error: error instanceof Error ? error.message : String(error) });
147
152
  return c.json(
@@ -227,11 +232,6 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
227
232
  // at GLOBAL_RESULT_LIMIT so a single app can saturate the response
228
233
  // but no more.
229
234
  const effectiveProviderLimit = active.length === 1 ? Math.min(GLOBAL_RESULT_LIMIT, query.provider_limit * 3) : query.provider_limit;
230
- // The same request-wide budget covers signing and provider I/O.
231
- const providerDeadline = AbortSignal.timeout(PROVIDER_TIMEOUT_MS);
232
- // A client that stops reading a stream, such as the browser on the next keystroke, ends the search too.
233
- const streamCancelled = new AbortController();
234
- const fanoutSignal = AbortSignal.any([c.req.raw.signal, providerDeadline, streamCancelled.signal]);
235
235
  // Guard the whole issuance batch once, then reuse the prepared signer for
236
236
  // every target. This is one Postgres check per outer search request, not
237
237
  // one check per provider.
@@ -258,7 +258,7 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
258
258
  signer,
259
259
  issuer: signer.issuer,
260
260
  },
261
- { db, groupsAdmin: signer.groupsAdmin },
261
+ { db, groupsAdmin: signer.groupsAdmin, signal: fanoutSignal },
262
262
  ),
263
263
  fanoutSignal,
264
264
  ),
@@ -353,6 +353,8 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
353
353
  ref: view.ref,
354
354
  title: view.title,
355
355
  href: open.href,
356
+ context: view.context,
357
+ status: view.status,
356
358
  preview: view.preview,
357
359
  icon: view.icon,
358
360
  priority: view.priority,
@@ -114,6 +114,8 @@ export const appPwaDeviceOperation = <I, O>(definition: {
114
114
  run: async (context, input) => parse(definition.output, await definition.handler(context, parse(definition.input, input))),
115
115
  });
116
116
 
117
+ export type AppPwaCoreQuery = "address.autocomplete";
118
+
117
119
  export type AppPwaOptions = {
118
120
  app: { id: string; installable: Pick<AppInstallable, "path"> };
119
121
  /** Authoritative configured public origin, never a Host/Forwarded/request header. HTTPS is mandatory. */
@@ -131,6 +133,8 @@ export type AppPwaOptions = {
131
133
  * Called before issuance and again before commit, including issuance retries. */
132
134
  authorizeVisit(signIn: AppPwaWebSignIn, subject: unknown, tx: TransactionSQL): Promise<boolean>;
133
135
  queries?: Readonly<Record<string, AppPwaQuery>>;
136
+ /** Read-only platform queries available through the closed guest Core-query route. Empty by default. */
137
+ coreQueries?: readonly AppPwaCoreQuery[];
134
138
  commands?: Readonly<Record<string, AppPwaCommand>>;
135
139
  deviceOperations?: Readonly<Record<string, AppPwaDeviceOperation>>;
136
140
  /** Missing operation declarations deny access; operation registration alone grants nothing. */
@@ -0,0 +1,20 @@
1
+ import type { GeoError } from "../services/geo/errors";
2
+
3
+ /** Guest-safe errors from the allowlisted platform adapter, never from application handlers. */
4
+ export class AppPwaCoreQueryError extends Error {
5
+ readonly code: GeoError["code"];
6
+ readonly status: GeoError["status"];
7
+ readonly details?: { scope: "person" | "cloud"; resetsAt: string };
8
+ constructor(error: GeoError) {
9
+ super(error.code);
10
+ this.code = error.code;
11
+ this.status = error.status;
12
+ if (
13
+ error.code === "GEO_BUDGET_EXHAUSTED" &&
14
+ (error.details.scope === "person" || error.details.scope === "cloud") &&
15
+ typeof error.details.resetsAt === "string"
16
+ ) {
17
+ this.details = { scope: error.details.scope, resetsAt: error.details.resetsAt };
18
+ }
19
+ }
20
+ }
@@ -2,6 +2,7 @@ export {
2
2
  APP_PWA_LIMITS,
3
3
  type AppPwaActor,
4
4
  type AppPwaCommandContext,
5
+ type AppPwaCoreQuery,
5
6
  type AppPwaDeviceActor,
6
7
  type AppPwaDeviceContext,
7
8
  type AppPwaDeviceView,
@@ -1,10 +1,17 @@
1
1
  import { sql, type TransactionSQL } from "bun";
2
2
  import { z } from "zod";
3
+ import { GeoAutocompleteInputSchema } from "../contracts/geo";
3
4
  import { isInstallablePath } from "../contracts/installable";
4
5
  import { PWA_LIMITS } from "../contracts/pwa";
6
+ import { ratelimit } from "../server/ratelimit";
7
+ import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
5
8
  import { getEffectivePermission, hasPermission } from "../server/services/access";
6
9
  import { audit } from "../services/audit";
10
+ import { GeoError } from "../services/geo/errors";
11
+ import { coreGeo } from "../services/geo/service";
12
+ import { geoPlan, readGeoConfig } from "../services/geo/settings";
7
13
  import { pairingSecret } from "../services/pairing-secret";
14
+ import { get as getSetting } from "../services/settings";
8
15
  import { databaseClock, lockWebAuthority } from "./authority";
9
16
  import {
10
17
  type AppPwaActor,
@@ -22,9 +29,11 @@ import {
22
29
  parseAppPwaInput as parse,
23
30
  pwaFail,
24
31
  } from "./contracts";
32
+ import { AppPwaCoreQueryError } from "./core-query-error";
25
33
  import { appPwaQr, parseAppPwaQr } from "./protocol";
26
34
  import { appPwaSchemaName, setupAppPwa } from "./schema";
27
35
  import { withAppPwaTransaction } from "./transaction";
36
+ import { untilAppPwaAbort } from "./transport-input";
28
37
 
29
38
  type Browser = { credential_hash: string; device_id: string | null; expires_at: Date };
30
39
  type Device = {
@@ -121,6 +130,9 @@ const liveVisit = (visit: Visit, time: Date) =>
121
130
  export const createAppPwaService = (options: AppPwaOptions) => {
122
131
  if (typeof options.authorizeStaff !== "function" || typeof options.authorizeVisit !== "function")
123
132
  throw new Error("App-PWA requires staff and visit-subject authorization");
133
+ const coreQueries = new Set(options.coreQueries ?? []);
134
+ if (!Array.isArray(options.coreQueries ?? []) || [...coreQueries].some((query) => query !== "address.autocomplete"))
135
+ throw new Error("Unsupported App-PWA Core query");
124
136
  const db = options.db ?? sql;
125
137
  const appId = options.app.id;
126
138
  const path = options.app.installable.path;
@@ -343,6 +355,32 @@ export const createAppPwaService = (options: AppPwaOptions) => {
343
355
  if (visit.device_id !== device.id || visit.browser_hash !== device.browser_hash) return pwaFail("NOT_FOUND", 404);
344
356
  return visit;
345
357
  };
358
+ const coreActor = async (tx: TransactionSQL, device: Device, beforeCommit: BeforeCommit, visitId?: string): Promise<AppPwaActor> => {
359
+ const currentVisit = visitId ?? device.active_visit_id;
360
+ if (!currentVisit) return deviceActor(device);
361
+ const visit = await boundVisit(tx, device, currentVisit);
362
+ if (device.active_visit_id !== visit.id) return pwaFail("REVOKED", 410);
363
+ // Device mode resumes after a visit ends. Explicit visit requests never fall back.
364
+ if (!visitId && !liveVisit(visit, await databaseClock(tx))) return deviceActor(device);
365
+ beforeCommit.deadline((time) => checkVisit(visit, time));
366
+ checkVisit(visit, await databaseClock(tx));
367
+ return visitActor(device, visit);
368
+ };
369
+ const recordCoreQuery = (actor: AppPwaActor, outcome: "allowed" | "denied" | "failed", code?: string, tx?: TransactionSQL) =>
370
+ audit.record(
371
+ {
372
+ action: "app_pwa.core.query",
373
+ outcome,
374
+ target: { type: "core.query", id: "address.autocomplete" },
375
+ ...(code ? { error: { code } } : {}),
376
+ metadata: {
377
+ appId,
378
+ subject: { type: "user", userId: actor.ownerUserId },
379
+ via: { kind: actor.kind, appId, deviceId: actor.deviceId, ...(actor.kind === "visit" ? { visitId: actor.visitId } : {}) },
380
+ },
381
+ },
382
+ tx,
383
+ );
346
384
  const readAttempt = async (tx: TransactionSQL, visitId: string, attemptId: string) => {
347
385
  const [row] = await tx<Attempt[]>`SELECT route, input_digest, result FROM ${tables.attempts}
348
386
  WHERE app_id = ${appId} AND visit_id = ${visitId}::uuid AND attempt_id = ${parse(AppPwaId, attemptId)}::uuid`;
@@ -627,6 +665,59 @@ export const createAppPwaService = (options: AppPwaOptions) => {
627
665
  },
628
666
  signal,
629
667
  ),
668
+ coreQuery: async (
669
+ credential: string,
670
+ visitId: string | undefined,
671
+ query: string,
672
+ input: unknown,
673
+ signal?: AbortSignal,
674
+ locale?: string,
675
+ ) => {
676
+ const requestSignal = AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(REQUEST_IDLE_TIMEOUT_SECONDS * 1000)]);
677
+ return untilAppPwaAbort(requestSignal, async () => {
678
+ if (query !== "address.autocomplete" || !coreQueries.has(query)) return pwaFail("FORBIDDEN", 403);
679
+ const value = parse(GeoAutocompleteInputSchema, bounded(input, options.budgets.requestBytes));
680
+ const actor = await guest(
681
+ credential,
682
+ (tx, device, _signal, beforeCommit) => coreActor(tx, device, beforeCommit, visitId),
683
+ requestSignal,
684
+ );
685
+ try {
686
+ // No pairing transaction or settings lock spans a pool checkout or provider request.
687
+ // Bound each device/visit by both the operator's request budget and the Geo plan's RPS.
688
+ const [requestRate, geoConfig] = await Promise.all([getSetting<number>("security.rate_limit_per_second"), readGeoConfig()]);
689
+ if (requestSignal.aborted) return pwaFail("UNAVAILABLE", 503);
690
+ const limiter = ratelimit({ id: `app-pwa:${appId}:core`, limit: Math.min(requestRate, geoPlan(geoConfig).rps) });
691
+ if ((await limiter.check(`device:${actor.deviceId}`)).limited) return pwaFail("RATE_LIMITED", 429);
692
+ if (actor.kind === "visit" && (await limiter.check(`visit:${actor.visitId}`)).limited) return pwaFail("RATE_LIMITED", 429);
693
+ if (requestSignal.aborted) return pwaFail("UNAVAILABLE", 503);
694
+ const result = bounded(await coreGeo.autocompleteForAppPwa(value, { actor, signal: requestSignal, locale }));
695
+ await guest(
696
+ credential,
697
+ async (tx, device, _signal, beforeCommit) => {
698
+ const current = await coreActor(tx, device, beforeCommit, actor.kind === "visit" ? actor.visitId : undefined);
699
+ // A newly opened visit changes device-mode authority; retry with its current status.
700
+ if (current.kind !== actor.kind) return pwaFail("UNAVAILABLE", 503);
701
+ if (current.deviceId !== actor.deviceId || current.ownerUserId !== actor.ownerUserId) return pwaFail("REVOKED", 410);
702
+ await recordCoreQuery(current, "allowed", undefined, tx);
703
+ return null;
704
+ },
705
+ requestSignal,
706
+ );
707
+ return result;
708
+ } catch (error) {
709
+ const code = error instanceof AppPwaError || error instanceof GeoError ? error.code : "UNAVAILABLE";
710
+ await recordCoreQuery(
711
+ actor,
712
+ error instanceof AppPwaError || (error instanceof GeoError && error.status < 500) ? "denied" : "failed",
713
+ code,
714
+ );
715
+ if (error instanceof GeoError) throw new AppPwaCoreQueryError(error);
716
+ if (error instanceof AppPwaError) throw error;
717
+ return pwaFail("UNAVAILABLE", 503);
718
+ }
719
+ });
720
+ },
630
721
  query: (credential: string, visitId: string, route: string, input: unknown, signal?: AbortSignal) =>
631
722
  guest(
632
723
  credential,
@@ -1,11 +1,13 @@
1
1
  import { type Context, Hono } from "hono";
2
2
  import { setCookie } from "hono/cookie";
3
3
  import { z } from "zod";
4
+ import { resolveLocale } from "../server/locale";
4
5
  import type { AuthContext } from "../server/middleware/auth";
5
6
  import { rateLimit } from "../server/middleware/rate-limit";
6
7
  import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
7
8
  import { pairingSecret } from "../services/pairing-secret";
8
9
  import { session } from "../services/session";
10
+ import { get as getSetting } from "../services/settings";
9
11
  import {
10
12
  AppPwaAttempt,
11
13
  AppPwaClaim,
@@ -17,6 +19,7 @@ import {
17
19
  parseAppPwaInput as parse,
18
20
  pwaFail,
19
21
  } from "./contracts";
22
+ import { AppPwaCoreQueryError } from "./core-query-error";
20
23
  import { type AppPwaService, createAppPwaService } from "./service";
21
24
  import { readAppPwaJson, untilAppPwaAbort } from "./transport-input";
22
25
 
@@ -57,6 +60,11 @@ export const createAppPwaRouter = (service: AppPwaService) => {
57
60
  const router = new Hono<AuthContext>();
58
61
  const limiter = rateLimit({ keyBy: "ip" });
59
62
  router.onError((error, c) => {
63
+ if (error instanceof AppPwaCoreQueryError)
64
+ return Response.json(
65
+ { code: error.code, ...(error.details ? { details: error.details } : {}) },
66
+ { status: error.status, headers: c.res.headers },
67
+ );
60
68
  const safe = error instanceof AppPwaError ? error : new AppPwaError("UNAVAILABLE", 503);
61
69
  return c.json({ code: safe.code }, safe.status);
62
70
  });
@@ -130,6 +138,20 @@ export const createAppPwaRouter = (service: AppPwaService) => {
130
138
  const input = parse(z.strictObject({ visitId: AppPwaId, attemptId: AppPwaId }), await body(c));
131
139
  return c.json(await service.attempt(key, input.visitId, input.attemptId, c.req.raw.signal));
132
140
  });
141
+ router.post(`${guestBase}/core/query/:query`, async (c) => {
142
+ const key = credential(c);
143
+ const input = parse(z.strictObject({ visitId: AppPwaId.optional(), input: z.unknown() }), await body(c));
144
+ return c.json(
145
+ await service.coreQuery(
146
+ key,
147
+ input.visitId,
148
+ c.req.param("query"),
149
+ input.input,
150
+ c.req.raw.signal,
151
+ resolveLocale(c.req.raw.headers, await getSetting<string>("app.locale")),
152
+ ),
153
+ );
154
+ });
133
155
  router.post(`${guestBase}/query/:route`, async (c) => {
134
156
  const key = credential(c);
135
157
  const input = parse(queryInput, await body(c));
@@ -10,7 +10,7 @@ export const approvalMessages = i18n.define({
10
10
  rejectAllTitle: "Reject all open approvals?",
11
11
  rejectAllBody: ({ count }: { count: number }) =>
12
12
  `${count === 1 ? "The open Action does not run." : `None of the ${count.toLocaleString("en")} open Actions runs.`} Where Cloud asked, it continues with your rejection.`,
13
- emptyTitle: "Nothing is waiting for you.",
13
+ emptyTitle: "You're all set.",
14
14
  emptyBody: "When an Action needs your approval, it appears here and in the chat it comes from.",
15
15
  loading: "Loading approvals…",
16
16
  loadFailed: "Approvals could not be loaded.",
@@ -48,7 +48,7 @@ export const approvalMessages = i18n.define({
48
48
  rejectAllTitle: "Alle offenen Freigaben ablehnen?",
49
49
  rejectAllBody: ({ count }) =>
50
50
  `${count === 1 ? "Die offene Aktion läuft nicht." : `Keine der ${count.toLocaleString("de")} offenen Aktionen läuft.`} Wo Cloud gefragt hat, geht es mit deiner Ablehnung weiter.`,
51
- emptyTitle: "Nichts wartet auf dich.",
51
+ emptyTitle: "Alles erledigt.",
52
52
  emptyBody: "Wenn eine Aktion deine Freigabe braucht, erscheint sie hier und im Chat, aus dem sie kommt.",
53
53
  loading: "Freigaben werden geladen …",
54
54
  loadFailed: "Die Freigaben konnten nicht geladen werden.",