@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
@@ -0,0 +1,300 @@
1
+ import type { z } from "zod";
2
+ import type { AccessSubject, CapabilityExecutionContext, RequestActor } from "../../contracts";
3
+ import {
4
+ GeoAutocompleteDataSchema,
5
+ type GeoAutocompleteInput,
6
+ GeoAutocompleteInputSchema,
7
+ type GeoFunction,
8
+ GeoGeocodeDataSchema,
9
+ type GeoGeocodeInput,
10
+ GeoGeocodeInputSchema,
11
+ GeoPlaceSearchDataSchema,
12
+ type GeoPlaceSearchInput,
13
+ GeoPlaceSearchInputSchema,
14
+ GeoReverseDataSchema,
15
+ type GeoReverseInput,
16
+ GeoReverseInputSchema,
17
+ } from "../../contracts/geo";
18
+ import { createGeoBucket, GEO_TIMEOUT_MS } from "./bucket";
19
+ import { createGeoCache, GEO_MAX_JSON_BYTES, geoCacheKey } from "./cache";
20
+ import { geoReferenceCost, geoReserveCost } from "./costs";
21
+ import { GeoError, GeoProviderError, limitExceeded } from "./errors";
22
+ import { GeoapifyAdapter } from "./geoapify";
23
+ import { createGeoLedger, geoPeriods } from "./ledger";
24
+ import { cutGeoWindow, decodePolyline6, locateOnGeoLine, simplifyGeoLine } from "./polyline";
25
+ import { createGeoRouteService } from "./route-service";
26
+ import { type GeoConfig, geoPlan, readGeoConfig } from "./settings";
27
+
28
+ export type GeoContext = Partial<Pick<CapabilityExecutionContext, "actor" | "accessSubject" | "locale" | "signal" | "requestId">>;
29
+ export const geoIdentity = (context: GeoContext): { actor: RequestActor; accessSubject: AccessSubject } => {
30
+ const { actor, accessSubject } = context;
31
+ if (
32
+ !actor ||
33
+ !accessSubject ||
34
+ !(actor.kind === "user" ? actor.user?.id : actor.serviceAccount?.id) ||
35
+ !(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
36
+ )
37
+ throw new GeoError("GEO_FORBIDDEN");
38
+ return { actor, accessSubject };
39
+ };
40
+ const bound = (name: string, value: number, min: number, max: number, integer = false) => {
41
+ if (!Number.isFinite(value) || value > max || value < min || (integer && !Number.isInteger(value)))
42
+ limitExceeded(name, value < min ? min : max, value);
43
+ };
44
+ const parse = <T>(schema: z.ZodType<T>, input: unknown): T => {
45
+ const parsed = schema.safeParse(input);
46
+ if (!parsed.success) throw new GeoError("GEO_UNSUPPORTED_OPTION");
47
+ return parsed.data;
48
+ };
49
+ const country = (value?: string) => {
50
+ if (value !== undefined && !/^[a-z]{2}$/i.test(value)) throw new GeoError("GEO_UNSUPPORTED_OPTION");
51
+ };
52
+ const area = (value?: GeoPlaceSearchInput["area"]) => {
53
+ if (!value) return;
54
+ if (value.type === "circle") bound("area.radiusMeters", value.radiusMeters, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER);
55
+ else {
56
+ bound("area.west", value.west, -180, 180);
57
+ bound("area.east", value.east, -180, 180);
58
+ bound("area.south", value.south, -90, 90);
59
+ bound("area.north", value.north, -90, 90);
60
+ if (value.west >= value.east || value.south >= value.north) throw new GeoError("GEO_UNSUPPORTED_OPTION");
61
+ }
62
+ };
63
+ export const createGeoService = (
64
+ options: {
65
+ config?: () => Promise<GeoConfig>;
66
+ now?: () => Date;
67
+ adapter?: GeoapifyAdapter;
68
+ cache?: ReturnType<typeof createGeoCache>;
69
+ bucket?: ReturnType<typeof createGeoBucket>;
70
+ ledger?: ReturnType<typeof createGeoLedger>;
71
+ } = {},
72
+ ) => {
73
+ const now = options.now ?? (() => new Date());
74
+ const config = options.config ?? readGeoConfig;
75
+ const adapter = options.adapter ?? new GeoapifyAdapter({ now });
76
+ const cache = options.cache ?? createGeoCache();
77
+ const bucket = options.bucket ?? createGeoBucket();
78
+ const ledger = options.ledger ?? createGeoLedger();
79
+ const budgets = (settings: GeoConfig) => ({
80
+ timezone: settings.timezone,
81
+ monthlyCredits: settings.monthlyCredits,
82
+ personDailyCredits: settings.personDailyCredits,
83
+ });
84
+ const recordTransition = async (event: "monthly_budget_exhausted" | "provider_key_rejected", timezone: string, context: GeoContext) => {
85
+ const identity = geoIdentity(context);
86
+ try {
87
+ await ledger.recordTransition(event, timezone, { ...context, ...identity }, now());
88
+ } catch {
89
+ // The transaction rolls back the dedup row too, allowing the next refusal to record it.
90
+ // Preserve the original recovery details and paid/unknown costs on audit outages.
91
+ console.error("[geo] Audit transition could not be recorded", { event });
92
+ }
93
+ };
94
+ const configured = async (context: GeoContext) => {
95
+ geoIdentity(context);
96
+ const value = await config();
97
+ if (!value.apiKey.trim()) throw new GeoError("GEO_NOT_CONFIGURED");
98
+ return value;
99
+ };
100
+ const paid = async <T>(
101
+ fn: GeoFunction,
102
+ input: unknown,
103
+ context: GeoContext,
104
+ settings: GeoConfig,
105
+ schema: z.ZodType<T>,
106
+ request: (signal: AbortSignal) => Promise<T>,
107
+ limit?: number,
108
+ cacheInput = input,
109
+ empty: (value: T) => boolean = () => false,
110
+ ): Promise<T> => {
111
+ const identity = geoIdentity(context);
112
+ const locale = context.locale ?? "en";
113
+ const key = geoCacheKey(fn, cacheInput, locale);
114
+ const hit = await cache.get(key, schema);
115
+ if (hit !== null) return hit;
116
+ const deadline = AbortSignal.timeout(GEO_TIMEOUT_MS[fn]);
117
+ const signal = context.signal ? AbortSignal.any([context.signal, deadline]) : deadline;
118
+ const release = await bucket.acquire(fn, geoPlan(settings).rps);
119
+ let reserved: Awaited<ReturnType<typeof ledger.reserve>> | undefined;
120
+ let settlementAttempted = false;
121
+ const reference = geoReferenceCost({ fn, limit });
122
+ try {
123
+ // A canceled request which has not been sent never reserves provider credit.
124
+ signal.throwIfAborted();
125
+ reserved = await ledger.reserve(
126
+ identity.accessSubject,
127
+ budgets(settings),
128
+ geoReserveCost([{ fn, limit }]),
129
+ fn === "places" ? "places" : "address",
130
+ now(),
131
+ );
132
+ const response = await request(signal);
133
+ settlementAttempted = true;
134
+ await ledger.settle(reserved, reference);
135
+ const value = schema.safeParse(response);
136
+ if (!value.success || Buffer.byteLength(JSON.stringify({ data: response })) > GEO_MAX_JSON_BYTES)
137
+ throw new GeoProviderError("GEO_PROVIDER_FAILED", "known");
138
+ await cache.put(key, fn, value.data, empty(value.data));
139
+ return value.data;
140
+ } catch (cause) {
141
+ let error = cause instanceof GeoError ? cause : new GeoError("GEO_PROVIDER_FAILED");
142
+ if (error.code === "GEO_BUDGET_EXHAUSTED" && error.details.scope === "cloud") {
143
+ await recordTransition("monthly_budget_exhausted", settings.timezone, context);
144
+ }
145
+ if (reserved) {
146
+ const charge = cause instanceof GeoProviderError ? cause.charge : cause instanceof GeoError ? "known" : "unknown";
147
+ let settled = charge === "known" ? reference : 0;
148
+ let unknown = charge === "unknown" ? reserved.cost : 0;
149
+ if (!settlementAttempted) {
150
+ settlementAttempted = true;
151
+ try {
152
+ await ledger.settle(reserved, charge === "none" ? 0 : charge === "unknown" ? reserved.cost : reference);
153
+ } catch {
154
+ settled = 0;
155
+ unknown = reserved.cost;
156
+ }
157
+ }
158
+ if (error.code === "GEO_PROVIDER_FAILED" || error.code === "GEO_BUSY")
159
+ error = new GeoError(error.code, {
160
+ ...error.details,
161
+ costs: { settledCredits: settled / 1000, unknownCredits: unknown / 1000 },
162
+ });
163
+ }
164
+ if (cause instanceof GeoProviderError && cause.keyRejected) {
165
+ await recordTransition("provider_key_rejected", settings.timezone, context);
166
+ }
167
+ throw error;
168
+ } finally {
169
+ await release();
170
+ }
171
+ };
172
+ const routes = createGeoRouteService({ configured, adapter, cache, bucket, ledger, now, recordTransition });
173
+ return {
174
+ ...routes,
175
+ async autocomplete(input: GeoAutocompleteInput, context: GeoContext) {
176
+ const settings = await configured(context);
177
+ const value = parse(GeoAutocompleteInputSchema, input);
178
+ bound("text", value.text.length, 3, 300);
179
+ bound("text", value.text.trim().length, 3, 300);
180
+ bound("limit", value.limit ?? 5, 1, 5, true);
181
+ country(value.countryCode);
182
+ area(value.area);
183
+ return paid(
184
+ "autocomplete",
185
+ value,
186
+ context,
187
+ settings,
188
+ GeoAutocompleteDataSchema,
189
+ (signal) => adapter.autocomplete(value, settings.apiKey, context.locale ?? "en", signal),
190
+ value.limit,
191
+ { ...value, limit: value.limit ?? 5 },
192
+ (result) => !result.suggestions.length,
193
+ );
194
+ },
195
+ async geocode(input: GeoGeocodeInput, context: GeoContext) {
196
+ const settings = await configured(context);
197
+ const value = parse(GeoGeocodeInputSchema, input);
198
+ if (value.text !== undefined) {
199
+ bound("text", value.text.length, 1, 300);
200
+ bound("text", value.text.trim().length, 1, 300);
201
+ }
202
+ for (const [name, field] of Object.entries(value.fields ?? {}))
203
+ if (field !== undefined) bound(`fields.${name}`, field.length, 0, 200);
204
+ country(value.countryCode);
205
+ if (!!value.text?.trim() === !!Object.values(value.fields ?? {}).some((v) => v?.trim())) throw new GeoError("GEO_UNSUPPORTED_OPTION");
206
+ return paid("geocode", value, context, settings, GeoGeocodeDataSchema, async (signal) => {
207
+ const result = await adapter.geocode(value, settings.apiKey, context.locale ?? "en", signal);
208
+ if (!result.candidates.length) throw new GeoError("GEO_ADDRESS_NOT_FOUND");
209
+ if (result.candidates.length > 1) throw new GeoError("GEO_ADDRESS_AMBIGUOUS", { candidates: result.candidates });
210
+ return { address: result.candidates[0]!, provenance: result.provenance };
211
+ });
212
+ },
213
+ async reverse(input: GeoReverseInput, context: GeoContext) {
214
+ const settings = await configured(context);
215
+ const value = parse(GeoReverseInputSchema, input);
216
+ return paid("reverse", value, context, settings, GeoReverseDataSchema, async (signal) => {
217
+ const result = await adapter.reverse(value, settings.apiKey, context.locale ?? "en", signal);
218
+ if (!result.address) throw new GeoError("GEO_ADDRESS_NOT_FOUND");
219
+ return { address: result.address, provenance: result.provenance };
220
+ });
221
+ },
222
+ async searchPlaces(input: GeoPlaceSearchInput, context: GeoContext) {
223
+ const settings = await configured(context);
224
+ const value = parse(GeoPlaceSearchInputSchema, input);
225
+ bound("categories", value.categories.length, 1, 5, true);
226
+ bound("limit", value.limit ?? 20, 1, 20, true);
227
+ area(value.area);
228
+ const along = value.along;
229
+ if (along) {
230
+ bound("along.windowKm", along.toKm - along.fromKm, Number.MIN_VALUE, 200);
231
+ bound("along.fromKm", along.fromKm, 0, Number.MAX_SAFE_INTEGER);
232
+ bound("along.bufferMeters", along.bufferMeters ?? 1000, 100, 5000);
233
+ }
234
+ if ([value.near, value.area, value.along].filter(Boolean).length !== 1) throw new GeoError("GEO_UNSUPPORTED_OPTION");
235
+ const cut = along ? cutGeoWindow(decodePolyline6(along.polyline6), along.fromKm * 1000, along.toKm * 1000) : undefined;
236
+ const simplified = cut ? simplifyGeoLine(cut) : undefined;
237
+ const cacheInput = {
238
+ ...value,
239
+ limit: value.limit ?? 20,
240
+ categories: [...new Set(value.categories)],
241
+ ...(along ? { along: { ...along, bufferMeters: along.bufferMeters ?? 1000 } } : {}),
242
+ };
243
+ return paid<z.infer<typeof GeoPlaceSearchDataSchema>>(
244
+ "places",
245
+ value,
246
+ context,
247
+ settings,
248
+ GeoPlaceSearchDataSchema,
249
+ async (signal) => {
250
+ // Radial reduction and Douglas–Peucker can each move the line by one tolerance.
251
+ // Widen the provider corridor to cover the original cut; filter locally with the requested buffer.
252
+ const providerInput =
253
+ along && simplified
254
+ ? {
255
+ ...value,
256
+ // Twenty provider candidates still cost one credit; apply the caller's limit after local filtering.
257
+ limit: 20,
258
+ along: {
259
+ ...along,
260
+ bufferMeters: Math.min(100000, Math.ceil((along.bufferMeters ?? 1000) + 2 * simplified.toleranceMeters)),
261
+ },
262
+ }
263
+ : value;
264
+ const result = await adapter.places(providerInput, settings.apiKey, context.locale ?? "en", signal, simplified?.line);
265
+ if (!cut || !along) return result;
266
+ const places = result.places
267
+ .map((place) => {
268
+ const position = locateOnGeoLine(place.point, cut);
269
+ return { ...place, ...position, atMeters: along.fromKm * 1000 + position.atMeters };
270
+ })
271
+ .filter((place) => place.distanceToRouteMeters <= (along.bufferMeters ?? 1000))
272
+ .sort((a, b) => a.atMeters - b.atMeters);
273
+ return {
274
+ ...result,
275
+ places: places.slice(0, value.limit ?? 20),
276
+ simplificationToleranceMeters: simplified!.toleranceMeters,
277
+ truncated: result.places.length === 20 && places.length < (value.limit ?? 20),
278
+ };
279
+ },
280
+ value.limit,
281
+ cacheInput,
282
+ (result) => !result.places.length,
283
+ );
284
+ },
285
+ async availability(context: GeoContext) {
286
+ const { accessSubject } = geoIdentity(context);
287
+ const settings = await config();
288
+ if (!settings.apiKey.trim()) {
289
+ const periods = geoPeriods(now(), settings.timezone);
290
+ return {
291
+ configured: false,
292
+ person: { exhausted: false, resetsAt: periods.person.resetsAt },
293
+ cloud: { exhausted: false, resetsAt: periods.cloud.resetsAt },
294
+ };
295
+ }
296
+ return { configured: true, ...(await ledger.availability(accessSubject, budgets(settings), now())) };
297
+ },
298
+ };
299
+ };
300
+ export const coreGeo = createGeoService();
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { GEO_PLAN_IDS } from "../../contracts/geo-admin";
3
+ import { readStates } from "../settings/store";
4
+
5
+ export const GEO_PLANS = {
6
+ free: { dailyCredits: 3000, rps: 5 },
7
+ api10: { dailyCredits: 10000, rps: 12 },
8
+ api25: { dailyCredits: 25000, rps: 15 },
9
+ api50: { dailyCredits: 50000, rps: 20 },
10
+ api100: { dailyCredits: 100000, rps: 25 },
11
+ api250: { dailyCredits: 250000, rps: 30 },
12
+ } as const;
13
+ const ConfigSchema = z.object({
14
+ apiKey: z.string(),
15
+ timezone: z.string(),
16
+ plan: z.enum(GEO_PLAN_IDS),
17
+ customRps: z.number().int().positive(),
18
+ customDailyCredits: z.number().int().positive(),
19
+ monthlyCredits: z.number().int().nonnegative(),
20
+ personDailyCredits: z.number().int().nonnegative(),
21
+ });
22
+ export type GeoConfig = z.infer<typeof ConfigSchema>;
23
+ export const geoPlan = (config: GeoConfig) =>
24
+ config.plan === "custom" ? { rps: config.customRps, dailyCredits: config.customDailyCredits } : GEO_PLANS[config.plan];
25
+ export const readGeoConfig = async (): Promise<GeoConfig> => {
26
+ // Direct encrypted-row reads: configuration checks never touch Valkey or put this key in its settings cache.
27
+ const keys = [
28
+ "geo.api_key",
29
+ "geo.plan",
30
+ "geo.custom_rps",
31
+ "geo.custom_daily_credits",
32
+ "geo.monthly_budget_credits",
33
+ "geo.person_daily_credits",
34
+ "app.timezone",
35
+ ];
36
+ const values = await readStates(keys);
37
+ const unreadable = keys.filter((key) => key.startsWith("geo.") && values.get(key)?.stored === "unreadable");
38
+ if (unreadable.length) console.warn("[geo] Unreadable settings; geo is not configured", { keys: unreadable });
39
+ return ConfigSchema.parse({
40
+ // A fallback budget must never admit paid work over an unreadable stored value.
41
+ apiKey: unreadable.length ? "" : values.get("geo.api_key")?.value,
42
+ plan: values.get("geo.plan")?.value,
43
+ customRps: values.get("geo.custom_rps")?.value,
44
+ customDailyCredits: values.get("geo.custom_daily_credits")?.value,
45
+ monthlyCredits: values.get("geo.monthly_budget_credits")?.value,
46
+ personDailyCredits: values.get("geo.person_daily_credits")?.value,
47
+ timezone: values.get("app.timezone")?.value,
48
+ });
49
+ };
@@ -3,9 +3,12 @@ import { appAudienceRoles, hasAnyAppRole } from "../../_internal/app-roles";
3
3
  import { getApp, listApps } from "../../_internal/registry";
4
4
  import type { AppRegistryEntry } from "../../contracts/registry";
5
5
  import type { RequestActor } from "../../contracts/shared";
6
+ import type { RequestAuthority } from "../../server/middleware/auth";
6
7
  import { resolveAppPresentations } from "../../shared/app-presentation";
7
8
  import { type HelpArticleLink, helpLocaleChain, parseHelpReference } from "../../shared/help";
8
9
  import { lexHelpMarkdown, walkHelpTokens } from "../../shared/markdown";
10
+ import { canUseApp } from "../app-access";
11
+ import { discoverableApps } from "../app-discovery";
9
12
  import { logger } from "../logging";
10
13
  import { HELP_RESOURCE_PAGE_SIZE, type HelpSelection, hasHelpBm25, metadata, queryHelp } from "./store";
11
14
  import type { HelpReader } from "./types";
@@ -30,13 +33,16 @@ export const createHelpReader = (
30
33
  locale: string,
31
34
  viewer: RequestActor | undefined,
32
35
  dependencies: {
36
+ authority?: RequestAuthority | null;
33
37
  db?: SQL;
34
38
  listApps?: () => Promise<AppRegistryEntry[]>;
35
39
  getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
36
40
  } = {},
37
41
  ): HelpReader => {
38
42
  const db = dependencies.db ?? sql;
39
- const visible = (app: AppRegistryEntry | null): app is AppRegistryEntry => app !== null && canReadAppHelp(app, viewer);
43
+ const visible = (app: AppRegistryEntry | null): app is AppRegistryEntry =>
44
+ app !== null && (app.access !== undefined || canReadAppHelp(app, viewer));
45
+ const visibleApps = async (apps: (AppRegistryEntry | null)[]) => discoverableApps(dependencies.authority ?? null, apps.filter(visible));
40
46
  const selectionFor = (apps: AppRegistryEntry[]): HelpSelection[] =>
41
47
  apps.flatMap((app) =>
42
48
  app.help
@@ -52,12 +58,12 @@ export const createHelpReader = (
52
58
  );
53
59
  const select = async (appId?: string): Promise<HelpSelection[]> => {
54
60
  const apps = appId ? [await (dependencies.getApp ?? getApp)(appId)] : await (dependencies.listApps ?? listApps)();
55
- return selectionFor(resolveAppPresentations(apps.filter(visible), locale));
61
+ return selectionFor(resolveAppPresentations(await visibleApps(apps), locale));
56
62
  };
57
63
  return {
58
64
  async manifest(appId) {
59
65
  const app = await (dependencies.getApp ?? getApp)(appId);
60
- if (!visible(app) || !app.help) return null;
66
+ if (!visible(app) || !app.help || !(await canUseApp(dependencies.authority ?? null, app))) return null;
61
67
  const [corpus] = await db`SELECT 1 FROM help.corpora WHERE app_id=${app.id} AND manifest_hash=${app.help.manifestHash}`;
62
68
  if (!corpus) return null;
63
69
  const rows = await queryHelp(
@@ -110,10 +116,13 @@ export const createHelpReader = (
110
116
  const referencedApps = new Set([...references].map((reference) => reference.split("/")[0]));
111
117
  const apps = references.size
112
118
  ? resolveAppPresentations(
113
- (referencedApps.size === 1 && referencedApps.has(input.appId)
114
- ? [await (dependencies.getApp ?? getApp)(input.appId)]
115
- : await (dependencies.listApps ?? listApps)()
116
- ).filter((app): app is AppRegistryEntry => visible(app) && referencedApps.has(app.id)),
119
+ (
120
+ await visibleApps(
121
+ referencedApps.size === 1 && referencedApps.has(input.appId)
122
+ ? [await (dependencies.getApp ?? getApp)(input.appId)]
123
+ : await (dependencies.listApps ?? listApps)(),
124
+ )
125
+ ).filter((app) => referencedApps.has(app.id)),
117
126
  locale,
118
127
  )
119
128
  : [];
@@ -1,5 +1,6 @@
1
1
  import type { AppRegistryHelpSummary } from "../../contracts/registry";
2
2
  import type { RequestActor } from "../../contracts/shared";
3
+ import type { RequestAuthority } from "../../server/middleware/auth";
3
4
  import type { HelpAccessLevel, HelpArticleLink, HelpDocumentManifest, HelpKind } from "../../shared/help";
4
5
 
5
6
  export type HelpDocument = {
@@ -52,4 +53,8 @@ export type HelpReader = {
52
53
  search(input: HelpSearchInput): Promise<HelpMatch[]>;
53
54
  read(input: HelpReadInput): Promise<HelpArticle | null>;
54
55
  };
55
- export type HelpReaderFactory = (locale: string, viewer: RequestActor | undefined) => HelpReader;
56
+ export type HelpReaderFactory = (
57
+ locale: string,
58
+ viewer: RequestActor | undefined,
59
+ options?: { authority?: RequestAuthority | null },
60
+ ) => HelpReader;
@@ -28,7 +28,25 @@ export type ResolvedInvocationAuthority = {
28
28
  scopes: string[];
29
29
  };
30
30
 
31
- const provenance = (claims: CloudInvocationClaims): NonNullable<RequestActor["delegation"]> => ({
31
+ export type InvocationPrincipalClaims = Pick<
32
+ CloudInvocationClaims,
33
+ | "sub"
34
+ | "principal_type"
35
+ | "session_kind"
36
+ | "scopes"
37
+ | "credential_id"
38
+ | "exp"
39
+ | "act"
40
+ | "credential_kind"
41
+ | "jti"
42
+ | "request_id"
43
+ | "mandate_id"
44
+ | "mandate_revision"
45
+ | "workload_type"
46
+ | "workload_id"
47
+ >;
48
+
49
+ const provenance = (claims: InvocationPrincipalClaims): NonNullable<RequestActor["delegation"]> => ({
32
50
  kind: "invocation",
33
51
  callingAppId: claims.act.sub.slice("app:".length),
34
52
  credentialKind: claims.credential_kind,
@@ -58,7 +76,7 @@ const mapServiceAccount = (row: ServiceAccountRow): ServiceAccount => ({
58
76
  });
59
77
 
60
78
  export const resolveInvocationAuthority = async (
61
- claims: CloudInvocationClaims,
79
+ claims: InvocationPrincipalClaims,
62
80
  query: typeof sql = sql,
63
81
  groupsAdminOverride?: string[],
64
82
  ): Promise<ResolvedInvocationAuthority | null> => {
@@ -0,0 +1,67 @@
1
+ import type { SQL } from "bun";
2
+ import { getApp } from "../../_internal/registry";
3
+ import type { AppMeta } from "../../contracts/app";
4
+ import { AppAccessDeniedError } from "../../server/middleware/app-access";
5
+ import type { RequestAuthority } from "../../server/middleware/auth";
6
+ import { canUseApp } from "../app-access";
7
+ import { recordAppAccessDenied } from "../app-access-metrics";
8
+ import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS } from "./constants";
9
+ import { resolveInvocationAuthority } from "./invocation-actor";
10
+ import { type InvocationAuthority, signInvocationToken } from "./invocation-token";
11
+ import { readIdentityGroupsAdmin } from "./runtime-config";
12
+
13
+ /** Reload the principal while retaining the original credential's scopes and mobile-session restriction. */
14
+ export const resolveIssuanceAuthority = async (
15
+ authority: InvocationAuthority,
16
+ callingAppId: string,
17
+ query?: Parameters<typeof resolveInvocationAuthority>[1],
18
+ groupsAdmin?: readonly string[],
19
+ ): Promise<RequestAuthority | null> => {
20
+ const preparedGroups = groupsAdmin ?? (query ? await readIdentityGroupsAdmin(query) : undefined);
21
+ return resolveInvocationAuthority(
22
+ {
23
+ ...authority,
24
+ act: { sub: `app:${callingAppId}` },
25
+ jti: crypto.randomUUID(),
26
+ exp: Math.floor(Date.now() / 1_000) + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
27
+ },
28
+ query,
29
+ preparedGroups ? [...preparedGroups] : undefined,
30
+ );
31
+ };
32
+
33
+ type RegisteredAppAccessOptions = {
34
+ db?: SQL;
35
+ getApp?: (id: string) => Promise<Pick<AppMeta, "id" | "access"> | null>;
36
+ };
37
+
38
+ export const requireRegisteredAppAccess = async (
39
+ authority: RequestAuthority | null,
40
+ appId: string,
41
+ options: RegisteredAppAccessOptions = {},
42
+ ): Promise<void> => {
43
+ const app = await (options.getApp ?? getApp)(appId).catch(() => null);
44
+ if (!app || !authority || !(await canUseApp(authority, app, { db: options.db }))) {
45
+ if (app) recordAppAccessDenied(app.id);
46
+ throw new AppAccessDeniedError(app ?? { id: appId });
47
+ }
48
+ };
49
+
50
+ /** The production broker seam for capability, widget, search, stream, and inter-app issuance. */
51
+ export const issueInvocationToken = async (
52
+ params: Parameters<typeof signInvocationToken>[0],
53
+ dependencies: RegisteredAppAccessOptions & {
54
+ groupsAdmin?: readonly string[];
55
+ resolve?: typeof resolveIssuanceAuthority;
56
+ sign?: typeof signInvocationToken;
57
+ } = {},
58
+ ): ReturnType<typeof signInvocationToken> => {
59
+ const authority = await (dependencies.resolve ?? resolveIssuanceAuthority)(
60
+ params.authority,
61
+ params.callingAppId,
62
+ dependencies.db,
63
+ dependencies.groupsAdmin,
64
+ );
65
+ await requireRegisteredAppAccess(authority, params.targetAppId, dependencies);
66
+ return (dependencies.sign ?? signInvocationToken)(params);
67
+ };
@@ -378,7 +378,7 @@ export const prepareIdentitySigner = async (purpose: SigningKeyPurpose): Promise
378
378
  */
379
379
  export const withActiveIdentitySigner = async <T>(
380
380
  purpose: SigningKeyPurpose,
381
- callback: (signer: PreparedIdentitySigner & { issuer: string }, db: typeof sql) => Promise<T>,
381
+ callback: (signer: PreparedIdentitySigner & { issuer: string; readonly groupsAdmin?: readonly string[] }, db: typeof sql) => Promise<T>,
382
382
  options: { pool?: typeof sql; signal?: AbortSignal; timeoutMs?: number } = {},
383
383
  ): Promise<T> => {
384
384
  const timeoutMs = options.timeoutMs ?? 30_000;
@@ -404,7 +404,9 @@ export const withActiveIdentitySigner = async <T>(
404
404
  // holding a caller's transaction: every issuance needs just one connection.
405
405
  for (let attempt = 0; attempt < 2; attempt += 1) {
406
406
  const signer = await wait("signer refresh", () => prepareIdentitySigner(purpose));
407
- const { issuer } = await wait("identity settings", getIdentityRuntimeConfig);
407
+ const { issuer, groupsAdmin } = await wait("identity settings", getIdentityRuntimeConfig);
408
+ // Keep this issuance independent of cache expiry while it waits for a connection or key lock.
409
+ const preparedGroupsAdmin = Object.freeze([...groupsAdmin]);
408
410
  const run = async (db: typeof sql): Promise<{ active: false } | { active: true; result: T }> => {
409
411
  // A caller released at its deadline must not sign once a connection frees up.
410
412
  check();
@@ -422,7 +424,7 @@ export const withActiveIdentitySigner = async <T>(
422
424
  check();
423
425
  if (!active) return { active: false };
424
426
  step = "signing";
425
- const result = await callback({ ...signer, issuer }, db);
427
+ const result = await callback({ ...signer, issuer, groupsAdmin: preparedGroupsAdmin }, db);
426
428
  // Roll back what the callback wrote instead of committing a result the caller never gets.
427
429
  check();
428
430
  return { active: true, result };
@@ -1,6 +1,9 @@
1
+ import type { SQL } from "bun";
1
2
  import { env } from "../../config/env";
2
3
  import { publicCloudOrigin } from "../../shared/app-url";
3
4
  import { get as getSetting } from "../settings";
5
+ import { decryptValue } from "../settings/crypto";
6
+ import { SETTINGS_MAP, validateSettingValue } from "../settings/defaults";
4
7
  import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_OAUTH_JWKS_PATH, CLOUD_SESSION_JWKS_PATH, IDENTITY_REFRESH_TIMEOUT_MS } from "./constants";
5
8
 
6
9
  const RUNTIME_CONFIG_TTL_MS = 60_000;
@@ -29,6 +32,26 @@ const normalizeGroups = (value: unknown): string[] => {
29
32
  return groups.length > 0 ? groups : ["admins"];
30
33
  };
31
34
 
35
+ /** Transaction-local admin groups for callers that already hold a connection.
36
+ * Never fill the process/settings cache with an uncommitted setting.
37
+ */
38
+ export const readIdentityGroupsAdmin = async (db: SQL): Promise<readonly string[]> => {
39
+ const key = "freeipa.groups.admin";
40
+ const def = SETTINGS_MAP.get(key);
41
+ if (!def) throw new Error("Missing identity admin-group setting definition");
42
+ const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
43
+ let groups: unknown = def.default;
44
+ if (row) {
45
+ try {
46
+ const validated = validateSettingValue(def, await decryptValue(row.value));
47
+ if (validated.ok) groups = validated.value;
48
+ } catch {
49
+ // Match the settings store's unreadable-row fallback without mutating the row.
50
+ }
51
+ }
52
+ return Object.freeze(normalizeGroups(groups));
53
+ };
54
+
32
55
  const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL => {
33
56
  if (!transportOrigin) return new URL(path, issuer);
34
57
  const origin = new URL(transportOrigin);
@@ -1,4 +1,5 @@
1
1
  // biome-ignore-all assist/source/organizeImports: Preserve grouped service barrel exports.
2
+ export { appAccessAuthority, canUseApp, usableAppIds } from "./app-access";
2
3
  export { ipa } from "./ipa";
3
4
  export { accounts } from "./accounts";
4
5
  export { accountsAppService } from "./accounts";
@@ -227,6 +228,8 @@ export type { WebVitalsOverview, WebVitalsQuery, WebVitalMetric, WebVitalName }
227
228
 
228
229
  export { mail } from "./outgoing-mail";
229
230
  export type {
231
+ MailContent,
232
+ MailContentContext,
230
233
  MailProfile,
231
234
  MailAttachment,
232
235
  MailMessage,
@@ -257,3 +260,11 @@ export {
257
260
  type NewBrowserSignInNotifier,
258
261
  } from "./known-browsers";
259
262
  export { LASTING_ACCESS_SIGN_IN_SECONDS, requireConfirmedBrowserSignIn } from "./session/recent";
263
+
264
+ export { deviceOffersApproval } from "./approvals/device";
265
+
266
+ /** Core-owned geo implementation; applications use geoClient from the server entry. */
267
+ export { coreGeo, createGeoService } from "./geo/service";
268
+ export { migrate as migrateGeo } from "./geo/migrate";
269
+
270
+ export { coreGeoMap, createGeoMapService } from "./geo/map";