@k2b/cloud 0.41.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 (227) hide show
  1. package/package.json +6 -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-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. package/src/server/services/geo.ts +0 -215
@@ -22,6 +22,10 @@ import { toPgTextArray } from "../postgres";
22
22
  import { requestCacheRedis } from "../request-cache-redis";
23
23
  import { decryptValue, encryptValue } from "./crypto";
24
24
  import { SETTINGS, SETTINGS_MAP, type SettingDef, validateSettingValue } from "./defaults";
25
+ import { SettingUnreadableError } from "./errors";
26
+ import { assertGeoBudgetPair } from "./geo-budget";
27
+
28
+ export { SettingUnreadableError } from "./errors";
25
29
 
26
30
  const REDIS_KEY = (k: string) => `settings:${k}`;
27
31
 
@@ -30,16 +34,6 @@ type SqlClient = typeof sql;
30
34
  export type StoredSettingState = "none" | "readable" | "unreadable";
31
35
  export type SettingRead = { value: unknown; stored: StoredSettingState };
32
36
 
33
- export class SettingUnreadableError extends HTTPException {
34
- readonly code = "setting_unreadable" as const;
35
-
36
- constructor(readonly key: string) {
37
- const message = `The stored value of setting "${key}" cannot be read by this application. Replace it with a new value or reset it to the default.`;
38
- super(409, { message, res: Response.json({ code: "setting_unreadable", message }, { status: 409 }) });
39
- this.name = "SettingUnreadableError";
40
- }
41
- }
42
-
43
37
  /**
44
38
  * Forget cached values so every container re-reads on next access.
45
39
  *
@@ -83,8 +77,8 @@ const resolveFallback = (def: SettingDef | undefined): unknown => {
83
77
  */
84
78
  export const readKey = async (key: string): Promise<unknown> => (await bulkRead([key])).get(key);
85
79
 
86
- export const readKeyState = async (key: string): Promise<SettingRead> => {
87
- const state = (await readStates([key])).get(key);
80
+ export const readKeyState = async (key: string, db: SqlClient = sql): Promise<SettingRead> => {
81
+ const state = (await readStates([key], db)).get(key);
88
82
  if (!state) throw new Error(`Missing read state for setting "${key}"`);
89
83
  return state;
90
84
  };
@@ -155,10 +149,10 @@ export const bulkRead = async (keys: readonly string[]): Promise<Map<string, unk
155
149
  * State reads go directly to Postgres without reading or filling the cache;
156
150
  * value reads (`get`, snapshot) keep using the cache.
157
151
  */
158
- export const readStates = async (keys: readonly string[]): Promise<Map<string, SettingRead>> => {
152
+ export const readStates = async (keys: readonly string[], db: SqlClient = sql): Promise<Map<string, SettingRead>> => {
159
153
  const result = new Map<string, SettingRead>();
160
154
  if (keys.length === 0) return result;
161
- const rows = await sql<StoredRow[]>`SELECT key, value FROM settings.entries WHERE key = ANY(${toPgTextArray([...keys])}::text[])`;
155
+ const rows = await db<StoredRow[]>`SELECT key, value FROM settings.entries WHERE key = ANY(${toPgTextArray([...keys])}::text[])`;
162
156
  const byKey = new Map(rows.map((row) => [row.key, row]));
163
157
  for (const key of keys) {
164
158
  const row = byKey.get(key);
@@ -233,6 +227,8 @@ export const deleteLegacyKeys = async (extraKnownKeys: readonly string[] = []):
233
227
  * the caller explicitly replaces them. The row check and write share a transaction.
234
228
  * Lock with a no-op UPDATE so ROW EXCLUSIVE is acquired before the row lock,
235
229
  * preserving lock order with table-level SHARE locks (#1303 review).
230
+ * A caller passing `db` owns geo budget pair validation before mutations and
231
+ * cache invalidation after commit.
236
232
  */
237
233
  export const writeKey = async (key: string, value: unknown, db?: SqlClient, options?: { replaceUnreadable?: boolean }): Promise<void> => {
238
234
  const def = SETTINGS_MAP.get(key);
@@ -274,16 +270,28 @@ export const writeKey = async (key: string, value: unknown, db?: SqlClient, opti
274
270
  throw new Error(`Could not write setting "${key}" after 3 attempts`);
275
271
  };
276
272
  if (db) await write(db);
277
- else await sql.begin(write);
273
+ else
274
+ await sql.begin(async (tx) => {
275
+ await assertGeoBudgetPair(tx, { [key]: validated.value }, []);
276
+ await write(tx);
277
+ });
278
278
 
279
279
  // Passing a transaction means the caller owns invalidation, because the row
280
280
  // is not visible to anyone else until they commit.
281
281
  if (!db) await invalidateSettingsCache([key]);
282
282
  };
283
283
 
284
- /** Delete the DB row and invalidate Redis. See writeKey for the `db` contract. */
284
+ /** Delete the DB row and invalidate Redis. With `db`, the caller owns geo budget pair validation and post-commit invalidation. */
285
285
  export const deleteKey = async (key: string, db?: SqlClient): Promise<void> => {
286
- await (db ?? sql)`DELETE FROM settings.entries WHERE key = ${key}`;
286
+ const remove = async (tx: SqlClient) => {
287
+ await tx`DELETE FROM settings.entries WHERE key = ${key}`;
288
+ };
289
+ if (db) await remove(db);
290
+ else
291
+ await sql.begin(async (tx) => {
292
+ await assertGeoBudgetPair(tx, {}, [key]);
293
+ await remove(tx);
294
+ });
287
295
  if (!db) await invalidateSettingsCache([key]);
288
296
  };
289
297
 
@@ -1,123 +1,41 @@
1
- import { err, fail, ok, type PageParams, type Paginated, type Result } from "@k2b/stdlib";
2
- import { type GeoPlace, geoService } from "../../server";
3
- import * as settings from "../settings";
1
+ import { err, fail, ok, type PageParams } from "@k2b/stdlib";
2
+ import { geoClient } from "../../capabilities/geo-client";
3
+ import type { CapabilityCaller } from "../../capabilities/server";
4
+ import type { GeoProvenance } from "../../contracts/geo";
4
5
 
5
6
  export const WEATHER_COUNTRY_CODE = "DE";
7
+ export type WeatherCity = { name: string; lat: number; lon: number; country?: string; state?: string; provenance: GeoProvenance };
6
8
 
7
- export type WeatherCity = {
8
- name: string;
9
- lat: number;
10
- lon: number;
11
- country?: string;
12
- state?: string;
13
- };
14
-
15
- const unavailable = <T>(): Result<T> =>
16
- fail({
17
- code: "WEATHER_CITY_SEARCH_UNAVAILABLE",
18
- message: "Weather city search is unavailable",
19
- status: 500,
20
- });
21
-
22
- /**
23
- * Adapts generic geo place data to the weather app's city model.
24
- */
25
- const mapPlace = (place: GeoPlace): WeatherCity => ({
26
- name: place.name,
27
- lat: place.lat,
28
- lon: place.lon,
29
- country: place.country,
30
- state: place.state,
31
- });
32
-
33
- /**
34
- * Resolves and validates the configured geo service base URL.
35
- */
36
- const getGeoBaseUrl = async (): Promise<Result<string>> => {
37
- const geoUrl = (await settings.get<string>("weather.geo_url")).trim();
38
- if (!geoUrl) {
39
- return fail({
40
- code: "WEATHER_CITY_SEARCH_NOT_CONFIGURED",
41
- message: "Geo API URL is not configured. Set weather.geo_url.",
42
- status: 500,
43
- });
44
- }
45
- return ok(geoUrl.replace(/\/$/, ""));
46
- };
47
-
48
- /**
49
- * Searches city candidates via the geo backend and enforces weather-specific country constraints.
50
- */
9
+ /** Weather adapts Core's public address contract; only Core owns provider access and budgets. */
51
10
  const list = async (config: {
11
+ caller: CapabilityCaller;
52
12
  pagination?: PageParams;
53
13
  signal?: AbortSignal;
54
- filter: {
55
- query: string;
56
- country?: string;
57
- };
58
- }): Promise<Result<Paginated<WeatherCity>>> => {
14
+ filter: { query: string; country?: string };
15
+ }) => {
59
16
  const query = config.filter.query.trim();
60
- if (!query) {
61
- return ok({
62
- items: [],
63
- page: config.pagination?.page ?? 1,
64
- perPage: config.pagination?.perPage ?? 20,
65
- total: 0,
66
- hasNext: false,
67
- });
68
- }
69
-
70
- const requestedCountry = config.filter.country?.trim().toUpperCase() ?? WEATHER_COUNTRY_CODE;
71
- if (requestedCountry !== WEATHER_COUNTRY_CODE) {
17
+ const page = config.pagination?.page ?? 1;
18
+ const perPage = Math.min(5, config.pagination?.perPage ?? 5);
19
+ if ((config.filter.country?.trim().toUpperCase() ?? WEATHER_COUNTRY_CODE) !== WEATHER_COUNTRY_CODE)
72
20
  return fail(err.badInput("Only German city search is supported (country=DE)."));
73
- }
74
-
75
- const baseUrlResult = await getGeoBaseUrl();
76
- if (!baseUrlResult.ok) return baseUrlResult;
77
- const geoUrl = baseUrlResult.data;
78
- const result = await geoService.place.list({
79
- baseUrl: geoUrl,
80
- pagination: config.pagination,
81
- signal: config.signal,
82
- filter: {
83
- query,
84
- country: WEATHER_COUNTRY_CODE,
85
- featureClass: "P",
21
+ if (query.length < 3 || page > 1) return ok({ items: [], page, perPage, total: 0, hasNext: false });
22
+ const result = await geoClient.autocomplete(
23
+ { text: query, countryCode: WEATHER_COUNTRY_CODE, type: "city", limit: perPage },
24
+ {
25
+ ...config.caller,
26
+ signal: config.signal ?? config.caller.signal,
86
27
  },
87
- });
88
- if (!result.ok) return unavailable();
89
-
90
- return ok({
91
- ...result.data,
92
- items: result.data.items.map(mapPlace),
93
- });
94
- };
95
-
96
- /**
97
- * Resolves one place by coordinates via the geo backend.
98
- */
99
- const get = async (config: { lat: number; lon: number }): Promise<Result<WeatherCity | null>> => {
100
- const baseUrlResult = await getGeoBaseUrl();
101
- if (!baseUrlResult.ok) return baseUrlResult;
102
- const result = await geoService.place.get({
103
- baseUrl: baseUrlResult.data,
104
- lat: config.lat,
105
- lon: config.lon,
106
- });
28
+ );
107
29
  if (!result.ok) return result;
108
-
109
- const place = result.data;
110
- if (!place) return ok(null);
111
- if (place.featureClass !== undefined && place.featureClass !== "P") {
112
- return ok(null);
113
- }
114
-
115
- return ok(mapPlace(place));
116
- };
117
-
118
- export const weatherCityService = {
119
- list,
120
- get,
30
+ const items = result.data.data.suggestions.map((address) => ({
31
+ name: address.city || address.formatted,
32
+ lat: address.point.lat,
33
+ lon: address.point.lon,
34
+ country: address.countryCode,
35
+ provenance: address.provenance,
36
+ ...(address.stateRegion ? { state: address.stateRegion } : {}),
37
+ }));
38
+ return ok({ items, page, perPage, total: items.length, hasNext: false });
121
39
  };
122
-
40
+ export const weatherCityService = { list };
123
41
  export type WeatherCityService = typeof weatherCityService;
@@ -1,11 +1,11 @@
1
- import { err, fail, ok, type Result } from "@k2b/stdlib";
1
+ import { err, fail, ok } from "@k2b/stdlib";
2
+ import type { CapabilityCaller } from "../../capabilities/server";
2
3
  import { registerSettings, toLegacySettingDefs } from "../settings/defaults";
3
4
  import { getCurrentWeather, getWeatherData } from "./forecast";
4
5
  import { WEATHER_COUNTRY_CODE } from "./geo";
5
6
  import { weatherLocationService } from "./location";
6
7
  import { weatherLocationsService } from "./locations";
7
8
  import { WEATHER_SETTINGS } from "./settings";
8
- import type { WeatherData } from "./types";
9
9
  import { weatherUiService } from "./ui";
10
10
 
11
11
  registerSettings(toLegacySettingDefs(WEATHER_SETTINGS));
@@ -13,13 +13,14 @@ registerSettings(toLegacySettingDefs(WEATHER_SETTINGS));
13
13
  /**
14
14
  * Resolves a city name to coordinates and returns weather for the first match.
15
15
  */
16
- const getByCityName = async (config: { query: string }): Promise<Result<WeatherData>> => {
16
+ const getByCityName = async (config: { query: string; caller: CapabilityCaller }) => {
17
17
  const query = config.query.trim();
18
18
  if (!query) {
19
19
  return fail(err.badInput("City query is required"));
20
20
  }
21
21
 
22
22
  const cityResult = await weatherLocationService.city.list({
23
+ caller: config.caller,
23
24
  pagination: { page: 1, perPage: 1 },
24
25
  filter: {
25
26
  query,
@@ -1,5 +1,6 @@
1
1
  import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
2
2
  import { sql } from "bun";
3
+ import type { GeoProvenance } from "../../contracts/geo";
3
4
  import { logger } from "../logging";
4
5
  import { withWeatherLocationShortId } from "./short-id";
5
6
 
@@ -11,6 +12,7 @@ export type Location = {
11
12
  state: string | null;
12
13
  lat: number;
13
14
  lon: number;
15
+ provenance?: GeoProvenance | null;
14
16
  };
15
17
 
16
18
  /**
@@ -23,6 +25,7 @@ const create = async (config: {
23
25
  state?: string;
24
26
  lat: number;
25
27
  lon: number;
28
+ provenance?: GeoProvenance;
26
29
  };
27
30
  }): Promise<Result<Location>> => {
28
31
  try {
@@ -30,10 +33,10 @@ const create = async (config: {
30
33
  await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.weather.location-short-id-backfill'))`;
31
34
  return withWeatherLocationShortId(async (shortId) => {
32
35
  const [row] = await tx`
33
- INSERT INTO weather_locations (short_id, user_id, name, state, lat, lon)
34
- VALUES (${shortId}, ${config.userId}, ${config.data.name}, ${config.data.state ?? null}, ${config.data.lat}, ${config.data.lon})
36
+ INSERT INTO weather_locations (short_id, user_id, name, state, lat, lon, provenance)
37
+ VALUES (${shortId}, ${config.userId}, ${config.data.name}, ${config.data.state ?? null}, ${config.data.lat}, ${config.data.lon}, (${config.data.provenance ? JSON.stringify(config.data.provenance) : null}::text)::jsonb)
35
38
  ON CONFLICT (short_id) DO NOTHING
36
- RETURNING short_id AS id, name, state, lat, lon
39
+ RETURNING short_id AS id, name, state, lat, lon, provenance
37
40
  `;
38
41
  return (row as Location | undefined) ?? null;
39
42
  });
@@ -93,7 +96,7 @@ const list = async (config: { userId: string; pagination?: PageParams; filter?:
93
96
  `;
94
97
 
95
98
  const items = (await sql`
96
- SELECT short_id AS id, name, state, lat, lon
99
+ SELECT short_id AS id, name, state, lat, lon, provenance
97
100
  FROM weather_locations
98
101
  WHERE user_id = ${config.userId}
99
102
  AND ${queryMatch}
@@ -118,7 +121,7 @@ const list = async (config: { userId: string; pagination?: PageParams; filter?:
118
121
  */
119
122
  const get = async (config: { id: string; userId: string }): Promise<Location | null> => {
120
123
  const [location] = await sql`
121
- SELECT short_id AS id, name, state, lat, lon
124
+ SELECT short_id AS id, name, state, lat, lon, provenance
122
125
  FROM weather_locations
123
126
  WHERE short_id = ${config.id} AND user_id = ${config.userId}
124
127
  `;
@@ -14,6 +14,7 @@ export const migrate = async (): Promise<void> => {
14
14
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
15
15
  )
16
16
  `.simple();
17
+ await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS provenance JSONB`.simple();
17
18
  console.log(" ✓ weather_locations table");
18
19
 
19
20
  await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS short_id TEXT`.simple();
@@ -47,20 +47,4 @@ export const WEATHER_SETTINGS = {
47
47
  },
48
48
  },
49
49
  },
50
- "weather.geo_url": {
51
- kind: "url",
52
- label: "Geo API URL",
53
- default: "",
54
- description: "Geocoding API URL for the location search feature",
55
- placeholder: "e.g. https://geocoding.example.com/search",
56
- presentation: {
57
- translations: {
58
- de: {
59
- label: "Geo-API-URL",
60
- description: "Geocoding-API-URL für die Ortssuche",
61
- placeholder: "z. B. https://geocoding.example.com/search",
62
- },
63
- },
64
- },
65
- },
66
50
  } as const;
@@ -0,0 +1,5 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`rendering keeps rendering a stored version 1 config byte for byte 1`] = `"7ac4c8231955f18292bccc1574886dff7fafb1453d6cbaf6d58f02452b65c4ff"`;
4
+
5
+ exports[`rendering keeps rendering a stored version 1 config byte for byte 2`] = `"f2d19e9d678d902f9e7dcb85e0eb28ee95caaf28bd32e063d7dfd7bf21a7d4a6"`;