@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -1,4 +1,5 @@
1
1
  import type { Sync } from "@k2b/sync";
2
+ import type { AppAccessRule } from "./app-access";
2
3
  import type { PlatformPermission } from "./outgoing-mail";
3
4
  import type { AppRegistryHelpSummary } from "./registry";
4
5
  import type { Role } from "./shared";
@@ -15,7 +16,7 @@ export type LegalLink = {
15
16
  icon?: string;
16
17
  };
17
18
 
18
- /** Static page links for the global search UI, ranked after resource results.
19
+ /** Static page links for the global search UI, shown before resource results.
19
20
  * These declare navigation only; they do not create capabilities or readers.
20
21
  */
21
22
  export type AppSearchLink = {
@@ -24,6 +25,7 @@ export type AppSearchLink = {
24
25
  description?: string;
25
26
  href: string;
26
27
  icon?: string;
28
+ /** Alternate search terms. At `nav.href`, these also match the automatic app launcher. */
27
29
  keywords?: readonly string[];
28
30
  };
29
31
 
@@ -121,6 +123,7 @@ export type AppPanel = {
121
123
  };
122
124
 
123
125
  export type AppMeta = {
126
+ access?: AppAccessRule;
124
127
  platformPermissions?: readonly PlatformPermission[];
125
128
  id: string;
126
129
  name: string;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { APP_APPROVAL_LIMITS } from "./app-approval";
3
- import { CapabilityActionReviewDataSchema, CapabilityAppIdSchema } from "./capabilities";
3
+ import { CapabilityActionReviewPresentationSchema, CapabilityAppIdSchema } from "./capabilities";
4
4
 
5
5
  export { APPROVAL_COUNTS_PATH, APPROVALS_API_PATH, APPROVALS_PAGE_PATH } from "./approval-paths";
6
6
 
@@ -42,7 +42,7 @@ export const PendingApprovalSchema = z
42
42
  /** What the person reads: the first line names the app and the Action, later lines add `Label: value` details. */
43
43
  sentence: z.string(),
44
44
  /** The owning app's review as the person saw it when Cloud asked. */
45
- review: CapabilityActionReviewDataSchema.nullable(),
45
+ review: CapabilityActionReviewPresentationSchema.nullable(),
46
46
  /** Where the request waits. The title is chat content, written by a person or a model, never Cloud's own words. */
47
47
  requester: z.object({ kind: z.literal("chat"), appId: z.string(), title: z.string().nullable(), href: z.string() }).strict(),
48
48
  /** Ways the person may remember this approval, the same the chat card offers. */
@@ -0,0 +1,186 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * The shape set of built avatars: a person's comic avatar and an agent's robot. A stored config names one option per
5
+ * part by a stable id. Ids are never removed or renamed once released, so every stored config keeps rendering; new
6
+ * options only add ids. `v` changes only when the schema itself breaks, and each version keeps its own schema and
7
+ * drawing.
8
+ */
9
+ export const AVATAR_SHAPE_SET_VERSION = 1;
10
+
11
+ export const PERSON_FACES = ["round", "oval", "square"] as const;
12
+ export const PERSON_SKIN_TONES = ["s1", "s2", "s3", "s4", "s5", "s6"] as const;
13
+ /** Every hairstyle passed a human review for unwanted associations; see the review list in the renderer tests. */
14
+ export const PERSON_HAIRSTYLES = ["none", "short", "buzz", "curly", "afro", "long", "bob", "bun", "locs"] as const;
15
+ export const PERSON_HAIR_COLORS = ["black", "dark", "brown", "blond", "red", "grey", "white", "blue", "pink"] as const;
16
+ export const PERSON_EYES = ["dots", "ovals", "happy", "wink", "calm"] as const;
17
+ export const PERSON_BROWS = ["none", "straight", "arched", "thick", "raised"] as const;
18
+ export const PERSON_MOUTHS = ["smile", "grin", "neutral", "oh", "smirk"] as const;
19
+ export const PERSON_BEARDS = ["none", "stubble", "moustache", "goatee", "full"] as const;
20
+ export const PERSON_GLASSES = ["none", "round", "square", "half", "sun"] as const;
21
+ export const PERSON_HEADWEAR = ["none", "beanie", "cap", "hijab", "turban", "kippa", "phones"] as const;
22
+ export const PERSON_EXTRAS = ["none", "freckles", "blush", "earrings", "mole"] as const;
23
+ export const AVATAR_BACKGROUNDS = ["sky", "mint", "sand", "rose", "lilac", "stone", "peach", "teal"] as const;
24
+
25
+ export const ROBOT_HEADS = ["square", "round", "capsule", "tv"] as const;
26
+ export const ROBOT_ANTENNAS = ["none", "ball", "double", "dish"] as const;
27
+ export const ROBOT_EYES = ["round", "visor", "pixel", "cyclops", "happy"] as const;
28
+ export const ROBOT_MOUTHS = ["grill", "smile", "led", "line", "none"] as const;
29
+ export const ROBOT_BODIES = ["plain", "panel", "light", "buttons"] as const;
30
+ export const ROBOT_COLORS = ["steel", "white", "blue", "teal", "coral", "yellow", "purple", "graphite"] as const;
31
+
32
+ export const PersonAvatarSchema = z
33
+ .object({
34
+ v: z.literal(AVATAR_SHAPE_SET_VERSION),
35
+ kind: z.literal("person"),
36
+ face: z.enum(PERSON_FACES),
37
+ skin: z.enum(PERSON_SKIN_TONES),
38
+ hair: z.enum(PERSON_HAIRSTYLES),
39
+ hairColor: z.enum(PERSON_HAIR_COLORS),
40
+ eyes: z.enum(PERSON_EYES),
41
+ brows: z.enum(PERSON_BROWS),
42
+ mouth: z.enum(PERSON_MOUTHS),
43
+ beard: z.enum(PERSON_BEARDS),
44
+ glasses: z.enum(PERSON_GLASSES),
45
+ head: z.enum(PERSON_HEADWEAR),
46
+ extra: z.enum(PERSON_EXTRAS),
47
+ bg: z.enum(AVATAR_BACKGROUNDS),
48
+ })
49
+ .strict();
50
+ export type PersonAvatar = z.infer<typeof PersonAvatarSchema>;
51
+
52
+ export const RobotAvatarSchema = z
53
+ .object({
54
+ v: z.literal(AVATAR_SHAPE_SET_VERSION),
55
+ kind: z.literal("robot"),
56
+ head: z.enum(ROBOT_HEADS),
57
+ antenna: z.enum(ROBOT_ANTENNAS),
58
+ eyes: z.enum(ROBOT_EYES),
59
+ mouth: z.enum(ROBOT_MOUTHS),
60
+ body: z.enum(ROBOT_BODIES),
61
+ color: z.enum(ROBOT_COLORS),
62
+ bg: z.enum(AVATAR_BACKGROUNDS),
63
+ })
64
+ .strict();
65
+ export type RobotAvatar = z.infer<typeof RobotAvatarSchema>;
66
+
67
+ export const AvatarConfigSchema = z.discriminatedUnion("kind", [PersonAvatarSchema, RobotAvatarSchema]);
68
+ export type AvatarConfig = z.infer<typeof AvatarConfigSchema>;
69
+
70
+ /** Which picture others see: an uploaded photo, the built avatar, or the fallback (initials for people). */
71
+ export const AVATAR_KINDS = ["photo", "built", "initials"] as const;
72
+ export type AvatarKind = (typeof AVATAR_KINDS)[number];
73
+
74
+ /** A stored config is a handful of short ids; anything larger than this is not a config. */
75
+ export const MAX_AVATAR_CONFIG_BYTES = 1024;
76
+
77
+ /** The JSON text whose hash identifies a built avatar: keys in schema order, so equal configs hash equally. */
78
+ export const canonicalAvatarConfig = (config: AvatarConfig): string =>
79
+ JSON.stringify(config.kind === "person" ? PersonAvatarSchema.parse(config) : RobotAvatarSchema.parse(config));
80
+
81
+ type Random = () => number;
82
+
83
+ const pick = <T>(list: readonly T[], random: Random): T => list[Math.floor(random() * list.length) % list.length] as T;
84
+
85
+ /** A first draft for a person: random, with a calm smile and nothing on top, so the person starts from a plain face. */
86
+ export const startPersonAvatar = (random: Random = Math.random): PersonAvatar => ({
87
+ v: AVATAR_SHAPE_SET_VERSION,
88
+ kind: "person",
89
+ face: pick(PERSON_FACES, random),
90
+ skin: pick(PERSON_SKIN_TONES, random),
91
+ hair: pick(PERSON_HAIRSTYLES.slice(1), random),
92
+ hairColor: pick(PERSON_HAIR_COLORS.slice(0, 6), random),
93
+ eyes: pick(PERSON_EYES, random),
94
+ brows: pick(PERSON_BROWS.slice(1), random),
95
+ mouth: "smile",
96
+ beard: "none",
97
+ glasses: "none",
98
+ head: "none",
99
+ extra: "none",
100
+ bg: pick(AVATAR_BACKGROUNDS, random),
101
+ });
102
+
103
+ /** Headwear the random button may choose; religious headwear is only ever the person's own choice. */
104
+ export const RANDOM_PERSON_HEADWEAR = ["beanie", "cap", "phones"] as const;
105
+
106
+ /** "Random" in the builder: everything changes except the skin tone, which stays the person's choice. */
107
+ export const randomizePersonAvatar = (current: PersonAvatar, random: Random = Math.random): PersonAvatar => {
108
+ const maybe = <T extends string>(chance: number, list: readonly T[]): T | "none" => (random() < chance ? pick(list, random) : "none");
109
+ return {
110
+ v: AVATAR_SHAPE_SET_VERSION,
111
+ kind: "person",
112
+ face: pick(PERSON_FACES, random),
113
+ skin: current.skin,
114
+ hair: pick(PERSON_HAIRSTYLES.slice(1), random),
115
+ hairColor: random() < 0.85 ? pick(PERSON_HAIR_COLORS.slice(0, 7), random) : pick(PERSON_HAIR_COLORS, random),
116
+ eyes: pick(PERSON_EYES, random),
117
+ brows: pick(PERSON_BROWS.slice(1), random),
118
+ mouth: pick(PERSON_MOUTHS, random),
119
+ beard: maybe(0.3, PERSON_BEARDS.slice(1)),
120
+ glasses: maybe(0.35, PERSON_GLASSES.slice(1)),
121
+ head: maybe(0.25, RANDOM_PERSON_HEADWEAR),
122
+ extra: maybe(0.35, PERSON_EXTRAS.slice(1)),
123
+ bg: pick(AVATAR_BACKGROUNDS, random),
124
+ };
125
+ };
126
+
127
+ export const randomRobotAvatar = (random: Random = Math.random): RobotAvatar => ({
128
+ v: AVATAR_SHAPE_SET_VERSION,
129
+ kind: "robot",
130
+ head: pick(ROBOT_HEADS, random),
131
+ antenna: pick(ROBOT_ANTENNAS, random),
132
+ eyes: pick(ROBOT_EYES, random),
133
+ mouth: pick(ROBOT_MOUTHS, random),
134
+ body: pick(ROBOT_BODIES, random),
135
+ color: pick(ROBOT_COLORS, random),
136
+ bg: pick(AVATAR_BACKGROUNDS, random),
137
+ });
138
+
139
+ /** A small seeded generator (cyrb53 seed, mulberry32 stream): the same seed draws the same robot everywhere. */
140
+ export const seededRandom = (seed: string): Random => {
141
+ let h1 = 0xdeadbeef;
142
+ let h2 = 0x41c6ce57;
143
+ for (let index = 0; index < seed.length; index++) {
144
+ const code = seed.charCodeAt(index);
145
+ h1 = Math.imul(h1 ^ code, 2654435761);
146
+ h2 = Math.imul(h2 ^ code, 1597334677);
147
+ }
148
+ let state = (Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)) >>> 0;
149
+ return () => {
150
+ state = (state + 0x6d2b79f5) >>> 0;
151
+ let t = state;
152
+ t = Math.imul(t ^ (t >>> 15), t | 1);
153
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
154
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
155
+ };
156
+ };
157
+
158
+ /**
159
+ * The parts a seed draws from, frozen at shape set 1. New robot options join the builder, never this list, so an
160
+ * agent's robot never changes when the shape set grows.
161
+ */
162
+ const ROBOT_SEED_PARTS_V1 = {
163
+ head: ["square", "round", "capsule", "tv"],
164
+ antenna: ["none", "ball", "double", "dish"],
165
+ eyes: ["round", "visor", "pixel", "cyclops", "happy"],
166
+ mouth: ["grill", "smile", "led", "line", "none"],
167
+ body: ["plain", "panel", "light", "buttons"],
168
+ color: ["steel", "white", "blue", "teal", "coral", "yellow", "purple", "graphite"],
169
+ bg: ["sky", "mint", "sand", "rose", "lilac", "stone", "peach", "teal"],
170
+ } as const satisfies { [Part in Exclude<keyof RobotAvatar, "v" | "kind">]: readonly RobotAvatar[Part][] };
171
+
172
+ /** The robot an agent shows until someone changes it: drawn from its account id, so it never changes by itself. */
173
+ export const robotAvatarFromSeed = (seed: string): RobotAvatar => {
174
+ const random = seededRandom(seed);
175
+ return {
176
+ v: AVATAR_SHAPE_SET_VERSION,
177
+ kind: "robot",
178
+ head: pick(ROBOT_SEED_PARTS_V1.head, random),
179
+ antenna: pick(ROBOT_SEED_PARTS_V1.antenna, random),
180
+ eyes: pick(ROBOT_SEED_PARTS_V1.eyes, random),
181
+ mouth: pick(ROBOT_SEED_PARTS_V1.mouth, random),
182
+ body: pick(ROBOT_SEED_PARTS_V1.body, random),
183
+ color: pick(ROBOT_SEED_PARTS_V1.color, random),
184
+ bg: pick(ROBOT_SEED_PARTS_V1.bg, random),
185
+ };
186
+ };
@@ -8,6 +8,7 @@ import {
8
8
  export * from "./capability-streams";
9
9
 
10
10
  import { z } from "zod";
11
+ import type { RequestAuthority } from "../server/middleware/auth";
11
12
  import { CommandPathSchema } from "./commands";
12
13
  import type { AccessSubject, RequestActor, User } from "./shared";
13
14
 
@@ -177,6 +178,9 @@ export type CapabilityReach = (typeof CAPABILITY_REACHES)[number];
177
178
  export const CAPABILITY_ACTION_OUTCOMES = ["running", "done", "rejected", "not_run", "unknown"] as const;
178
179
  export type CapabilityActionOutcome = (typeof CAPABILITY_ACTION_OUTCOMES)[number];
179
180
  export const CAPABILITY_MAX_REVIEW_BYTES = 32 * 1024;
181
+ /** Full review text and metadata, separate from the compact card and ordinary capability results. */
182
+ export const CAPABILITY_MAX_REVIEW_CONTENT_BYTES = 16 * 1024 * 1024;
183
+ export const CAPABILITY_MAX_REVIEW_RESPONSE_BYTES = CAPABILITY_MAX_REVIEW_CONTENT_BYTES + CAPABILITY_MAX_REVIEW_BYTES + 11;
180
184
  export const CapabilityActionPolicySchema = z.union([
181
185
  z.object({ silent: z.enum(CAPABILITY_REACHES), routines: z.literal(false).optional() }).strict(),
182
186
  z.object({ ask: z.enum(CAPABILITY_ASK_REASONS) }).strict(),
@@ -188,14 +192,14 @@ const reviewLine = (max: number, min = 1) =>
188
192
  .string()
189
193
  .min(min)
190
194
  .max(max)
191
- .refine((text) => !/[\p{Cc}\p{Cf}\u2028\u2029]/u.test(text), "Contains unsafe characters");
195
+ .refine((text) => !/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u.test(text), "Contains unsafe characters");
192
196
  const reviewBlock = (max: number) =>
193
197
  z
194
198
  .string()
195
199
  .max(max)
196
- .refine((text) => !/[\p{Cc}\p{Cf}\u2028\u2029]/u.test(text.replace(/[\n\t]/g, "")), "Contains unsafe characters");
200
+ .refine((text) => !/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u.test(text.replace(/[\n\t]/g, "")), "Contains unsafe characters");
197
201
  export const CapabilityPersonSchema = z
198
- .object({ name: reviewLine(120).optional(), address: reviewLine(320).optional(), userId: z.uuid().optional() })
202
+ .object({ name: reviewLine(200).optional(), address: reviewLine(320).optional(), userId: z.uuid().optional() })
199
203
  .strict()
200
204
  .refine((person) => person.name !== undefined || person.address !== undefined, "A person needs a name or address");
201
205
  export type CapabilityPerson = z.infer<typeof CapabilityPersonSchema>;
@@ -432,60 +436,143 @@ export const CapabilityPreviewSchema = z.discriminatedUnion("kind", [
432
436
  .strict(),
433
437
  ]);
434
438
  export type CapabilityPreview = z.infer<typeof CapabilityPreviewSchema>;
435
- export const CapabilityActionCallReviewSchema = z
436
- .object({
437
- locale: z.string(),
438
- sentence: z
439
- .object({
440
- action: reviewTemplate,
441
- running: reviewTemplate,
442
- done: reviewTemplate,
443
- confirm: reviewTemplate,
444
- decline: reviewTemplate,
445
- rejected: reviewTemplate.optional(),
446
- notRun: reviewTemplate.optional(),
447
- short: reviewTemplate.refine((text) => text.length <= 60, "short exceeds 60 characters").optional(),
448
- })
449
- .strict(),
450
- slots: reviewSlots.readonly(),
451
- preview: CapabilityPreviewSchema.optional(),
452
- consequences: z.array(CapabilityConsequenceSchema).max(6).optional(),
453
- ask: z.enum(CAPABILITY_ASK_REASONS).optional(),
454
- reach: z.enum(CAPABILITY_REACHES).optional(),
455
- effects: z.number().int().min(1).max(1000).optional(),
456
- check: z
457
- .object({ query: CapabilityLocalIdSchema, input: z.record(z.string(), z.unknown()) })
458
- .strict()
459
- .optional(),
460
- scope: z.string().min(1).max(200).optional(),
461
- links: z.array(CapabilitySemanticLinkSchema).max(10).optional(),
462
- })
463
- .strict()
464
- .superRefine((review, ctx) => {
465
- for (const [form, text] of Object.entries(review.sentence)) {
466
- if (text === undefined) continue;
467
- const complete = form === "action" || form === "running" || form === "done";
468
- for (const message of capabilitySentenceIssues(text, review.slots, complete, form === "action"))
469
- ctx.addIssue({ code: "custom", path: ["sentence", form], message });
470
- if ((form === "confirm" || form === "decline") && [...text.matchAll(/\{[a-z][A-Za-z0-9]{0,31}\}/g)].length > 1)
471
- ctx.addIssue({ code: "custom", path: ["sentence", form], message: "Buttons name at most one slot" });
472
- }
473
- });
439
+ /** Complete passive content; no attachment bytes, HTML, omitted entries, or silently shortened strings. */
440
+ // Construct complete review validators in pure calls: unrelated browser consumers of resource refs do not need them.
441
+ export const CapabilityReviewContentSchema = /* @__PURE__ */ (() => {
442
+ const fullReviewPerson = z
443
+ .object({
444
+ name: z.string().max(200).optional(),
445
+ address: z.string().max(320).optional(),
446
+ userId: z.uuid().optional(),
447
+ })
448
+ .strict()
449
+ .refine((person) => person.name !== undefined || person.address !== undefined, "A person needs a name or address");
450
+ return z
451
+ .discriminatedUnion("kind", [
452
+ z
453
+ .object({
454
+ kind: z.literal("message"),
455
+ baseRevision: z.number().int().nonnegative().optional(),
456
+ from: fullReviewPerson.optional(),
457
+ to: z.array(fullReviewPerson).max(600),
458
+ cc: z.array(fullReviewPerson).max(600).optional(),
459
+ bcc: z.array(fullReviewPerson).max(600).optional(),
460
+ subject: z.string().max(998),
461
+ body: z.string().max(CAPABILITY_MAX_REVIEW_CONTENT_BYTES),
462
+ format: z.literal("plain"),
463
+ attachments: z
464
+ .array(
465
+ z
466
+ .object({
467
+ name: z.string().min(1).max(255),
468
+ size: z.number().int().nonnegative().optional(),
469
+ ref: CloudResourceRefSchema.optional(),
470
+ })
471
+ .strict(),
472
+ )
473
+ .max(202)
474
+ .optional(),
475
+ sendAt: reviewTime.optional(),
476
+ warnings: z.array(z.string().max(300)).max(6).optional(),
477
+ })
478
+ .strict(),
479
+ z
480
+ .object({
481
+ kind: z.literal("page"),
482
+ baseRevision: z.number().int().nonnegative().optional(),
483
+ title: z.string().max(200),
484
+ place: z.string().max(200).optional(),
485
+ body: z.string().max(CAPABILITY_MAX_REVIEW_CONTENT_BYTES),
486
+ format: z.literal("plain"),
487
+ })
488
+ .strict(),
489
+ ])
490
+ .superRefine((content, ctx) => {
491
+ if (content.kind === "message" && content.to.length + (content.cc?.length ?? 0) + (content.bcc?.length ?? 0) > 600)
492
+ ctx.addIssue({ code: "custom", message: "Full review content exceeds 600 recipients" });
493
+ if (new TextEncoder().encode(JSON.stringify(content)).byteLength > CAPABILITY_MAX_REVIEW_CONTENT_BYTES)
494
+ ctx.addIssue({ code: "custom", message: "Full review content exceeds 16 MiB" });
495
+ });
496
+ })();
497
+ export type CapabilityReviewContent = z.infer<typeof CapabilityReviewContentSchema>;
498
+ /** Cloud supplies this after storing a review. It is a locator, never an authorization token. */
499
+ export const CapabilityReviewContentRefSchema = /* @__PURE__ */ (() =>
500
+ z
501
+ .object({
502
+ href: z
503
+ .string()
504
+ .regex(
505
+ /^\/api\/approvals\/(?:[a-fA-F0-9-]{36}\/content|conversations\/[a-fA-F0-9-]{36}\/turns\/[a-fA-F0-9-]{36}\/reviews\/(?!\.{1,2}$)(?:[A-Za-z0-9_!~*'().-]|%[A-Fa-f0-9]{2})+)$/,
506
+ )
507
+ .max(2048),
508
+ digest: z.string().regex(/^[a-f0-9]{64}$/),
509
+ })
510
+ .strict())();
511
+ export type CapabilityReviewContentRef = z.infer<typeof CapabilityReviewContentRefSchema>;
512
+ export const CapabilityActionCallReviewSchema = /* @__PURE__ */ (() =>
513
+ z
514
+ .object({
515
+ locale: z.string(),
516
+ sentence: z
517
+ .object({
518
+ action: reviewTemplate,
519
+ running: reviewTemplate,
520
+ done: reviewTemplate,
521
+ confirm: reviewTemplate,
522
+ decline: reviewTemplate,
523
+ rejected: reviewTemplate.optional(),
524
+ notRun: reviewTemplate.optional(),
525
+ short: reviewTemplate.refine((text) => text.length <= 60, "short exceeds 60 characters").optional(),
526
+ })
527
+ .strict(),
528
+ slots: reviewSlots.readonly(),
529
+ preview: CapabilityPreviewSchema.optional(),
530
+ content: CapabilityReviewContentSchema.optional(),
531
+ consequences: z.array(CapabilityConsequenceSchema).max(6).optional(),
532
+ ask: z.enum(CAPABILITY_ASK_REASONS).optional(),
533
+ reach: z.enum(CAPABILITY_REACHES).optional(),
534
+ effects: z.number().int().min(1).max(1000).optional(),
535
+ check: z
536
+ .object({ query: CapabilityLocalIdSchema, input: z.record(z.string(), z.unknown()) })
537
+ .strict()
538
+ .optional(),
539
+ scope: z.string().min(1).max(200).optional(),
540
+ links: z.array(CapabilitySemanticLinkSchema).max(10).optional(),
541
+ })
542
+ .strict()
543
+ .superRefine((review, ctx) => {
544
+ for (const [form, text] of Object.entries(review.sentence)) {
545
+ if (text === undefined) continue;
546
+ const complete = form === "action" || form === "running" || form === "done";
547
+ for (const message of capabilitySentenceIssues(text, review.slots, complete, form === "action"))
548
+ ctx.addIssue({ code: "custom", path: ["sentence", form], message });
549
+ if ((form === "confirm" || form === "decline") && [...text.matchAll(/\{[a-z][A-Za-z0-9]{0,31}\}/g)].length > 1)
550
+ ctx.addIssue({ code: "custom", path: ["sentence", form], message: "Buttons name at most one slot" });
551
+ }
552
+ }))();
474
553
  export type CapabilityActionCallReview = z.infer<typeof CapabilityActionCallReviewSchema>;
475
554
  export type CapabilityActionReviewData = CapabilityActionCallReview | CapabilityActionReview;
476
- export const CapabilityActionReviewDataSchema = z
477
- .unknown()
478
- .transform((value, ctx) => {
479
- const schema =
480
- value !== null && typeof value === "object" && Object.hasOwn(value, "sentence")
481
- ? CapabilityActionCallReviewSchema
482
- : CapabilityActionReviewSchema;
483
- const parsed = schema.safeParse(value);
484
- if (parsed.success) return parsed.data;
485
- for (const issue of parsed.error.issues) ctx.addIssue({ code: "custom", path: issue.path, message: issue.message });
486
- return z.NEVER;
487
- })
488
- .pipe(z.union([CapabilityActionCallReviewSchema, CapabilityActionReviewSchema]));
555
+ export const CapabilityActionReviewDataSchema = /* @__PURE__ */ (() =>
556
+ z
557
+ .unknown()
558
+ .transform((value, ctx) => {
559
+ const schema =
560
+ value !== null && typeof value === "object" && Object.hasOwn(value, "sentence")
561
+ ? CapabilityActionCallReviewSchema
562
+ : CapabilityActionReviewSchema;
563
+ const parsed = schema.safeParse(value);
564
+ if (parsed.success) return parsed.data;
565
+ for (const issue of parsed.error.issues) ctx.addIssue({ code: "custom", path: issue.path, message: issue.message });
566
+ return z.NEVER;
567
+ })
568
+ .pipe(z.union([CapabilityActionCallReviewSchema, CapabilityActionReviewSchema])))();
569
+ /** Compact Cloud presentation. Providers return the strict review above, never a client-supplied content locator. */
570
+ export const CapabilityActionReviewPresentationSchema = /* @__PURE__ */ (() =>
571
+ z.union([
572
+ CapabilityActionCallReviewSchema.safeExtend({ contentRef: CapabilityReviewContentRefSchema.optional() }),
573
+ CapabilityActionReviewSchema,
574
+ ]))();
575
+ export type CapabilityActionReviewPresentation = z.infer<typeof CapabilityActionReviewPresentationSchema>;
489
576
  export const CapabilityActionReceiptSchema = z
490
577
  .object({ ...sentenceShape, preview: CapabilityPreviewSchema.optional() })
491
578
  .strict()
@@ -676,6 +763,12 @@ export const UniversalSearchDataSchema = z.array(CloudResourceViewSchema).max(10
676
763
  export type UniversalSearchData = z.infer<typeof UniversalSearchDataSchema>;
677
764
 
678
765
  export type CapabilityExecutionContext = {
766
+ /**
767
+ * Resolved request authority, supplied by the framework for execution, review
768
+ * and streams. Optional for existing in-process callers; authority-aware
769
+ * services fail closed when it is absent. Preserve its credential limits.
770
+ */
771
+ authority?: RequestAuthority;
679
772
  actor: RequestActor;
680
773
  accessSubject: AccessSubject;
681
774
  user: User | null;
@@ -695,6 +788,7 @@ export type CapabilityExecutionContext = {
695
788
  requestId: string;
696
789
  /** Cloud surface that invoked this capability. */
697
790
  origin: CapabilityOrigin;
791
+ /** Caller cancellation or shared dispatch deadline; forward it to expensive work. */
698
792
  signal: AbortSignal;
699
793
  };
700
794
 
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+
3
+ /** Geoapify plans in the order the administration offers them. */
4
+ export const GEO_PLAN_IDS = ["free", "api10", "api25", "api50", "api100", "api250", "custom"] as const;
5
+ export type GeoPlanId = (typeof GEO_PLAN_IDS)[number];
6
+
7
+ /**
8
+ * Ledger function groups in display order. Every paid geo call reserves under one of these groups;
9
+ * the administration shows one share line per group.
10
+ */
11
+ export const GEO_USAGE_GROUPS = ["tiles", "places", "route", "address"] as const;
12
+ export type GeoUsageGroup = (typeof GEO_USAGE_GROUPS)[number];
13
+
14
+ /** Largest credit value a geo budget setting accepts (stored as thousandths in a bigint ledger). */
15
+ export const GEO_MAX_CREDITS = Math.floor(Number.MAX_SAFE_INTEGER / 1000);
16
+ /** Largest custom request rate the token bucket accepts. */
17
+ export const GEO_MAX_RPS = Math.floor(Number.MAX_SAFE_INTEGER / 9000);
18
+ /** Upper bound of a pasted provider key; Geoapify keys are 32 characters. */
19
+ export const GEO_MAX_KEY_LENGTH = 256;
20
+
21
+ export type GeoAdminPerson = { id: string; name: string };
22
+
23
+ export type GeoAdminKey = {
24
+ /** `unreadable`: a stored key exists but this installation cannot decrypt it; geo is off until it is replaced or removed. */
25
+ state: "none" | "set" | "unreadable";
26
+ setAt: string | null;
27
+ setBy: GeoAdminPerson | null;
28
+ };
29
+
30
+ export type GeoAdminBudget = {
31
+ plan: GeoPlanId;
32
+ customRps: number;
33
+ customDailyCredits: number;
34
+ monthlyCredits: number;
35
+ personDailyCredits: number;
36
+ /** Budget settings whose stored value cannot be read; saving the form replaces them. */
37
+ unreadable: Array<GeoBudgetField | "plan">;
38
+ changedAt: string | null;
39
+ changedBy: GeoAdminPerson | null;
40
+ };
41
+
42
+ export type GeoAdminStatus = {
43
+ key: GeoAdminKey;
44
+ budget: GeoAdminBudget;
45
+ plans: Record<Exclude<GeoPlanId, "custom">, { dailyCredits: number; rps: number }>;
46
+ /** `app.timezone`: dates on the page and the budget periods use it. */
47
+ timezone: string;
48
+ };
49
+
50
+ export type GeoAdminUsage = {
51
+ /** Credits. Values may carry fractions (a map tile costs 0.25). */
52
+ month: { period: string; used: number; limit: number; resetsAt: string };
53
+ /** Cloud-wide use today against the provider's daily allowance of the selected plan. */
54
+ today: { period: string; used: number; allowance: number; resetsAt: string };
55
+ shares: Array<{ group: GeoUsageGroup; credits: number }>;
56
+ /** Access subjects whose use today reached the per-person daily limit. */
57
+ peopleAtLimit: number;
58
+ timezone: string;
59
+ };
60
+
61
+ export const GeoKeyInputSchema = z.object({ apiKey: z.string().max(GEO_MAX_KEY_LENGTH * 4) }).strict();
62
+
63
+ export const GeoBudgetInputSchema = z
64
+ .object({
65
+ plan: z.enum(GEO_PLAN_IDS),
66
+ customRps: z.unknown(),
67
+ customDailyCredits: z.unknown(),
68
+ monthlyCredits: z.unknown(),
69
+ personDailyCredits: z.unknown(),
70
+ })
71
+ .strict();
72
+ export type GeoBudgetInput = z.infer<typeof GeoBudgetInputSchema>;
73
+ export type GeoBudgetField = "customRps" | "customDailyCredits" | "monthlyCredits" | "personDailyCredits";
74
+
75
+ /** Stable field error codes; the page renders their text. */
76
+ export const GeoBudgetFieldErrorSchema = z.discriminatedUnion("code", [
77
+ z.object({ code: z.literal("not_whole_number") }),
78
+ z.object({ code: z.literal("too_small"), min: z.number() }),
79
+ z.object({ code: z.literal("too_large"), max: z.number() }),
80
+ z.object({ code: z.literal("above_monthly"), max: z.number() }),
81
+ ]);
82
+ export type GeoBudgetFieldError = z.infer<typeof GeoBudgetFieldErrorSchema>;
83
+ /** The 400 answer of a refused budget save. */
84
+ export const GeoBudgetRefusalSchema = z.object({
85
+ code: z.literal("geo_budget_invalid"),
86
+ errors: z.partialRecord(z.enum(["customRps", "customDailyCredits", "monthlyCredits", "personDailyCredits"]), GeoBudgetFieldErrorSchema),
87
+ });
88
+
89
+ export type GeoKeyError =
90
+ | { code: "geo_key_missing" }
91
+ | { code: "geo_key_malformed" }
92
+ | { code: "geo_key_rejected" }
93
+ | { code: "geo_key_unverified" };
94
+
95
+ /** Validate one budget number; `null` when it is acceptable. */
96
+ const checkNumber = (value: unknown, min: number, max: number): GeoBudgetFieldError | null => {
97
+ if (typeof value !== "number" || !Number.isInteger(value)) return { code: "not_whole_number" };
98
+ if (value < min) return { code: "too_small", min };
99
+ if (value > max) return { code: "too_large", max };
100
+ return null;
101
+ };
102
+
103
+ /** Check the whole form before anything is written; the same rules as the settings registry plus the pair rule. */
104
+ export const validateGeoBudget = (input: GeoBudgetInput) => {
105
+ const errors: Partial<Record<GeoBudgetField, GeoBudgetFieldError>> = {};
106
+ const fields: Array<[GeoBudgetField, number, number]> = [
107
+ ["monthlyCredits", 0, GEO_MAX_CREDITS],
108
+ ["personDailyCredits", 0, GEO_MAX_CREDITS],
109
+ ];
110
+ if (input.plan === "custom") fields.push(["customRps", 1, GEO_MAX_RPS], ["customDailyCredits", 1, GEO_MAX_CREDITS]);
111
+ for (const [field, min, max] of fields) {
112
+ const error = checkNumber(input[field], min, max);
113
+ if (error) errors[field] = error;
114
+ }
115
+ const month = input.monthlyCredits,
116
+ day = input.personDailyCredits;
117
+ if (!errors.monthlyCredits && !errors.personDailyCredits && typeof month === "number" && typeof day === "number" && day > month)
118
+ errors.personDailyCredits = { code: "above_monthly", max: month };
119
+ return errors;
120
+ };
121
+
122
+ /** Shape check before the provider sees a key: printable ASCII without spaces, as Geoapify issues them. */
123
+ export const geoKeyProblem = (apiKey: string): "geo_key_missing" | "geo_key_malformed" | null => {
124
+ if (!apiKey) return "geo_key_missing";
125
+ if (apiKey.length > GEO_MAX_KEY_LENGTH || !/^[\x21-\x7e]+$/.test(apiKey)) return "geo_key_malformed";
126
+ return null;
127
+ };
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+
3
+ export const GeoMapStyleInputSchema = z
4
+ .object({
5
+ theme: z.enum(["light", "dark"]).default("light").describe("Map appearance; labels use the request locale."),
6
+ })
7
+ .strict();
8
+ export const GeoMapStyleDataSchema = z
9
+ .object({
10
+ styleUrl: z.string().startsWith("/api/geo/style?"),
11
+ attribution: z.array(z.object({ label: z.string(), href: z.url() }).strict()),
12
+ })
13
+ .strict();
14
+ export const geoMapContract = { style: { id: "map.style", input: GeoMapStyleInputSchema, data: GeoMapStyleDataSchema } } as const;
15
+
16
+ // This is a transport memory budget, not an asserted provider maximum. At Free's
17
+ // 20 tile requests/s and 10 s deadline, buffered tile bodies occupy at most 200 MiB.
18
+ export const GEO_MAX_TILE_BYTES = 1024 * 1024;
19
+ export const GEO_MAP_MAX_ZOOM = 14;