@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
@@ -0,0 +1,248 @@
1
+ import { z } from "zod";
2
+
3
+ export const GeoPointSchema = z
4
+ .object({
5
+ lat: z.number().min(-90).max(90).describe("Latitude in degrees, -90 to 90."),
6
+ lon: z.number().min(-180).max(180).describe("Longitude in degrees, -180 to 180."),
7
+ })
8
+ .strict();
9
+ export const GeoProvenanceSchema = z
10
+ .object({
11
+ provider: z.literal("geoapify").describe("Source provider."),
12
+ attribution: z
13
+ .object({
14
+ text: z.literal("Geoapify · OpenStreetMap").describe("Attribution to display with this result."),
15
+ links: z.array(z.url()).length(2).describe("Provider and data-source attribution links."),
16
+ })
17
+ .strict()
18
+ .describe("Required attribution; preserve when storing or reusing the result."),
19
+ computedAt: z.iso.datetime().describe("ISO timestamp when the provider result was computed."),
20
+ })
21
+ .strict();
22
+ const text = z.string().max(300);
23
+ export const GeoAddressSchema = z
24
+ .object({
25
+ line1: text.describe("Street and house number."),
26
+ postalCode: text.describe("Postal code."),
27
+ city: text.describe("City or locality."),
28
+ stateRegion: text.describe("State or region."),
29
+ countryCode: z.string().max(2).describe("ISO alpha-2 country code, or empty when unavailable."),
30
+ point: GeoPointSchema.describe("Resolved geographic point."),
31
+ formatted: z.string().max(600).describe("Formatted provider address."),
32
+ confidence: z.number().min(0).max(1).nullable().describe("Provider match confidence, or null when unavailable."),
33
+ provenance: GeoProvenanceSchema.describe("Provider source and required attribution."),
34
+ })
35
+ .strict();
36
+
37
+ /** Verified Places categories; names belong to Cloud, provider keys to the adapter. */
38
+ export const GEO_CATEGORIES = {
39
+ museum: "entertainment.museum",
40
+ cafe: "catering.cafe",
41
+ restaurant: "catering.restaurant",
42
+ bar: "catering.bar",
43
+ pub: "catering.pub",
44
+ bakery: "commercial.food_and_drink.bakery",
45
+ supermarket: "commercial.supermarket",
46
+ pharmacy: "healthcare.pharmacy",
47
+ hotel: "accommodation.hotel",
48
+ accommodation: "accommodation",
49
+ fuel: "service.vehicle.fuel",
50
+ parking: "parking",
51
+ charging: "service.vehicle.charging_station",
52
+ toilets: "amenity.toilet",
53
+ park: "leisure.park",
54
+ viewpoint: "tourism.attraction.viewpoint",
55
+ attraction: "tourism.attraction",
56
+ } as const;
57
+ export const GeoCategorySchema = z.enum([
58
+ "museum",
59
+ "cafe",
60
+ "restaurant",
61
+ "bar",
62
+ "pub",
63
+ "bakery",
64
+ "supermarket",
65
+ "pharmacy",
66
+ "hotel",
67
+ "accommodation",
68
+ "fuel",
69
+ "parking",
70
+ "charging",
71
+ "toilets",
72
+ "park",
73
+ "viewpoint",
74
+ "attraction",
75
+ ]);
76
+ export const GeoAreaSchema = z.discriminatedUnion("type", [
77
+ z
78
+ .object({
79
+ type: z.literal("bbox").describe("Bounding box."),
80
+ west: z.number().describe("Western longitude."),
81
+ south: z.number().describe("Southern latitude."),
82
+ east: z.number().describe("Eastern longitude."),
83
+ north: z.number().describe("Northern latitude."),
84
+ })
85
+ .strict(),
86
+ z
87
+ .object({
88
+ type: z.literal("circle").describe("Circular search area."),
89
+ point: GeoPointSchema.describe("Center of the circle."),
90
+ radiusMeters: z.number().describe("Positive radius in meters."),
91
+ })
92
+ .strict(),
93
+ ]);
94
+ // Bounds are enforced by the service, after authentication/configuration, so all consumers receive GEO_LIMIT_EXCEEDED.
95
+ export const GeoAutocompleteInputSchema = z
96
+ .object({
97
+ text: z.string().describe("Partial address, 3–300 characters."),
98
+ limit: z.number().optional().describe("At most 5 suggestions; default 5."),
99
+ countryCode: z.string().optional().describe("Optional ISO alpha-2 country filter."),
100
+ type: z
101
+ .enum(["country", "state", "city", "postcode", "street", "amenity", "locality"])
102
+ .optional()
103
+ .describe("Optional address result type."),
104
+ bias: GeoPointSchema.optional().describe("Optional ranking point."),
105
+ area: GeoAreaSchema.optional().describe("Optional geographic filter."),
106
+ })
107
+ .strict();
108
+ export const GeoGeocodeInputSchema = z
109
+ .object({
110
+ text: z.string().optional().describe("Address text, at most 300 characters; use text or fields."),
111
+ fields: z
112
+ .object({
113
+ line1: z.string().optional().describe("Street and house number, at most 200 characters."),
114
+ postalCode: z.string().optional().describe("Postal code, at most 200 characters."),
115
+ city: z.string().optional().describe("City, at most 200 characters."),
116
+ stateRegion: z.string().optional().describe("State or region, at most 200 characters."),
117
+ country: z.string().optional().describe("Country name, at most 200 characters."),
118
+ })
119
+ .strict()
120
+ .optional()
121
+ .describe("Structured address, at most 200 characters per field."),
122
+ countryCode: z.string().optional().describe("Optional ISO alpha-2 country filter."),
123
+ })
124
+ .strict();
125
+ export const GeoReverseInputSchema = z.object({ point: GeoPointSchema.describe("Coordinates to reverse geocode.") }).strict();
126
+ // 102,400 ASCII bytes matches Geoapify's documented geometry budget, bounding decode memory/work before simplification.
127
+ export const GEO_MAX_POLYLINE_CHARS = 102_400;
128
+ export const GeoPlaceSearchInputSchema = z
129
+ .object({
130
+ categories: z.array(GeoCategorySchema).describe("1–5 Cloud categories."),
131
+ near: GeoPointSchema.optional().describe("Rank by distance to this point; choose near, area, or along."),
132
+ area: GeoAreaSchema.optional().describe("Optional geographic filter."),
133
+ along: z
134
+ .object({
135
+ polyline6: z.string().describe("Full route as polyline6; at most 102,400 ASCII characters."),
136
+ fromKm: z.number().describe("Start position on the full route in km."),
137
+ toKm: z.number().describe("End position; window at most 200 km."),
138
+ bufferMeters: z.number().optional().describe("100–5,000 m; default 1,000."),
139
+ })
140
+ .strict()
141
+ .optional()
142
+ .describe("Route corridor with a window of at most 200 km."),
143
+ limit: z.number().optional().describe("1–20 results; default 20."),
144
+ })
145
+ .strict();
146
+ export const GeoPlaceSchema = z
147
+ .object({
148
+ id: z.string().min(1).max(2048),
149
+ name: text,
150
+ categories: z.array(z.string().max(100)).max(20),
151
+ point: GeoPointSchema,
152
+ address: GeoAddressSchema,
153
+ distanceMeters: z.number().nonnegative().optional(),
154
+ atMeters: z.number().nonnegative().optional(),
155
+ distanceToRouteMeters: z.number().nonnegative().optional(),
156
+ openingHours: z.string().max(1000).optional(),
157
+ website: z.string().max(1000).optional(),
158
+ phone: text.optional(),
159
+ provenance: GeoProvenanceSchema,
160
+ })
161
+ .strict();
162
+ export const GeoAutocompleteDataSchema = z
163
+ .object({ suggestions: z.array(GeoAddressSchema).max(5), provenance: GeoProvenanceSchema })
164
+ .strict();
165
+ export const GeoGeocodeDataSchema = z.object({ address: GeoAddressSchema, provenance: GeoProvenanceSchema }).strict();
166
+ export const GeoReverseDataSchema = GeoGeocodeDataSchema;
167
+ export const GeoPlaceSearchDataSchema = z
168
+ .object({
169
+ places: z.array(GeoPlaceSchema).max(20),
170
+ provenance: GeoProvenanceSchema,
171
+ simplificationToleranceMeters: z.number().nonnegative().optional(),
172
+ truncated: z
173
+ .boolean()
174
+ .optional()
175
+ .describe("Candidate limit reached before filling the requested corridor results; narrow the distance window."),
176
+ })
177
+ .strict();
178
+ export const GeoAvailabilityInputSchema = z.object({}).strict();
179
+ const balance = z.object({ exhausted: z.boolean(), resetsAt: z.iso.datetime() }).strict();
180
+ export const GeoAvailabilityDataSchema = z.object({ configured: z.boolean(), person: balance, cloud: balance }).strict();
181
+ export const GeoCostsSchema = z.object({ settledCredits: z.number().nonnegative(), unknownCredits: z.number().nonnegative() }).strict();
182
+ export const GeoErrorDetailsSchema = z.discriminatedUnion("code", [
183
+ z.object({ code: z.literal("GEO_NOT_CONFIGURED") }).strict(),
184
+ z.object({ code: z.literal("GEO_FORBIDDEN") }).strict(),
185
+ z.object({ code: z.literal("GEO_UNSUPPORTED_OPTION") }).strict(),
186
+ z.object({ code: z.literal("GEO_ADDRESS_NOT_FOUND"), stopId: z.string().optional(), costs: GeoCostsSchema.optional() }).strict(),
187
+ z.object({ code: z.literal("GEO_NO_ROUTE"), costs: GeoCostsSchema.optional() }).strict(),
188
+ z
189
+ .object({
190
+ code: z.literal("GEO_COST_OVERRUN"),
191
+ distanceAllowanceMeters: z.number().positive(),
192
+ actualDistanceMeters: z.number().nonnegative(),
193
+ costs: GeoCostsSchema,
194
+ })
195
+ .strict(),
196
+ z.object({ code: z.literal("GEO_BUDGET_EXHAUSTED"), scope: z.enum(["person", "cloud"]), resetsAt: z.iso.datetime() }).strict(),
197
+ z.object({ code: z.literal("GEO_BUSY"), retryAfterSeconds: z.number().positive(), costs: GeoCostsSchema.optional() }).strict(),
198
+ z
199
+ .object({
200
+ code: z.literal("GEO_LIMIT_EXCEEDED"),
201
+ limit: z.string(),
202
+ max: z.number(),
203
+ actual: z.number(),
204
+ costs: GeoCostsSchema.optional(),
205
+ })
206
+ .strict(),
207
+ z
208
+ .object({
209
+ code: z.literal("GEO_ADDRESS_AMBIGUOUS"),
210
+ stopId: z.string().optional(),
211
+ costs: GeoCostsSchema.optional(),
212
+ candidates: z.array(GeoAddressSchema).max(5),
213
+ })
214
+ .strict(),
215
+ z
216
+ .object({
217
+ code: z.literal("GEO_PROVIDER_FAILED"),
218
+ costs: GeoCostsSchema,
219
+ adminHint: z.string().optional(),
220
+ accounting: z
221
+ .object({
222
+ reservationCredits: z.number().nonnegative(),
223
+ knownReferenceCredits: z.number().nonnegative(),
224
+ unknownReferenceCredits: z.number().nonnegative(),
225
+ })
226
+ .strict()
227
+ .optional()
228
+ .describe("Present when ledger settlement failed: retained reservation and reference costs requiring operator reconciliation."),
229
+ })
230
+ .strict(),
231
+ ]);
232
+ export type GeoPoint = z.infer<typeof GeoPointSchema>;
233
+ export type GeoProvenance = z.infer<typeof GeoProvenanceSchema>;
234
+ export type GeoAddress = z.infer<typeof GeoAddressSchema>;
235
+ export type GeoPlace = z.infer<typeof GeoPlaceSchema>;
236
+ export type GeoAutocompleteInput = z.infer<typeof GeoAutocompleteInputSchema>;
237
+ export type GeoGeocodeInput = z.infer<typeof GeoGeocodeInputSchema>;
238
+ export type GeoReverseInput = z.infer<typeof GeoReverseInputSchema>;
239
+ export type GeoPlaceSearchInput = z.infer<typeof GeoPlaceSearchInputSchema>;
240
+ export type GeoErrorCode = z.infer<typeof GeoErrorDetailsSchema>["code"];
241
+ export type GeoFunction = "autocomplete" | "geocode" | "reverse" | "places" | "route";
242
+ export const geoContract = {
243
+ autocomplete: { id: "address.autocomplete", input: GeoAutocompleteInputSchema, data: GeoAutocompleteDataSchema },
244
+ geocode: { id: "address.geocode", input: GeoGeocodeInputSchema, data: GeoGeocodeDataSchema },
245
+ reverse: { id: "address.reverse", input: GeoReverseInputSchema, data: GeoReverseDataSchema },
246
+ searchPlaces: { id: "place.search", input: GeoPlaceSearchInputSchema, data: GeoPlaceSearchDataSchema },
247
+ availability: { id: "geo.availability", input: GeoAvailabilityInputSchema, data: GeoAvailabilityDataSchema },
248
+ } as const;
@@ -7,6 +7,10 @@ export * from "./capability-compatibility";
7
7
  export * from "./commands";
8
8
  export * from "./contact-directory";
9
9
  export * from "./file-provider";
10
+ export * from "./geo";
11
+ export * from "./geo-admin";
12
+ export * from "./geo-map";
13
+ export * from "./geo-route";
10
14
  export * from "./geoip";
11
15
  export * from "./notification-types";
12
16
  export * from "./outgoing-mail";
@@ -5,7 +5,9 @@ import { type RequestActor, ServiceAccountSchema, UserSchema } from "./shared";
5
5
  export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read"]);
6
6
  export type PlatformPermission = z.infer<typeof PlatformPermissionSchema>;
7
7
  export const MailRetentionSchema = z.object({ contentDays: z.int().min(1).max(36500), recordDays: z.int().min(1).max(36500) }).strict();
8
- export type MailRetention = z.infer<typeof MailRetentionSchema>;
8
+ export type MailRetention = z.infer<typeof MailRetentionSchema> & {
9
+ unreadable?: Partial<Record<"outgoing_mail.content_retention_days" | "outgoing_mail.record_retention_days", boolean>>;
10
+ };
9
11
 
10
12
  export const MailProfileKeySchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
11
13
  const headerText = z
@@ -129,6 +131,8 @@ export const AdminMailAppSchema = z.object({
129
131
  readDeclared: z.boolean(),
130
132
  /** Other apps whose send log this app may read, besides its own. */
131
133
  logApps: z.array(z.string()),
134
+ /** Subset of logApps with explicit content access. Missing on older servers. */
135
+ logContentApps: z.array(z.string()).optional(),
132
136
  });
133
137
  export type AdminMailApp = z.infer<typeof AdminMailAppSchema>;
134
138
  /** Application IDs as registered, like sender access; Core's own mail is never readable by apps. */
@@ -136,11 +140,15 @@ export const MailLogAppIdSchema = z.string().min(1);
136
140
  /** How many other apps' send logs an administrator can let one app read. */
137
141
  const MAIL_LOG_GRANT_LIMIT = 100;
138
142
  export const MailLogAccessSchema = z
139
- .object({ apps: z.array(MailLogAppIdSchema).max(MAIL_LOG_GRANT_LIMIT) })
143
+ .object({
144
+ apps: z.array(MailLogAppIdSchema).max(MAIL_LOG_GRANT_LIMIT),
145
+ contentApps: z.array(MailLogAppIdSchema).max(MAIL_LOG_GRANT_LIMIT).optional(),
146
+ })
140
147
  .strict()
148
+ .refine((value) => new Set(value.apps).size === value.apps.length && !value.apps.includes("core"), "Choose distinct apps other than core")
141
149
  .refine(
142
- (value) => new Set(value.apps).size === value.apps.length && !value.apps.includes("core"),
143
- "Choose distinct apps other than core",
150
+ ({ apps, contentApps = [] }) => new Set(contentApps).size === contentApps.length && contentApps.every((id) => apps.includes(id)),
151
+ "Content apps must be a distinct subset of apps",
144
152
  );
145
153
  export type MailLogAccess = z.infer<typeof MailLogAccessSchema>;
146
154
 
@@ -315,3 +323,8 @@ export type MailServiceError = ServiceError<MailErrorCode> & {
315
323
  used?: number;
316
324
  requested?: number;
317
325
  };
326
+
327
+ /** One audited read; no HTML, headers, or attachment bytes. */
328
+ export type MailContent = { purged: true; contentPurgedAt: string } | { purged: false; text: string | null; attachmentNames: string[] };
329
+ /** Pass a currently authenticated server context, never browser JSON or a stored actor snapshot. */
330
+ export type MailContentContext = { actor: RequestActor; requestId?: string };
@@ -1,4 +1,5 @@
1
1
  import type { AppAdminNavigationGroup, AppAppearance, AppAppearanceColor, AppPanel, AppPresentationCatalog, AppSearchLink } from "./app";
2
+ import type { AppAccessRule } from "./app-access";
2
3
  import type { CapabilityManifest, CapabilityPresentationCatalog } from "./capabilities";
3
4
  import type { PlatformPermission } from "./outgoing-mail";
4
5
  import type { Role } from "./shared";
@@ -77,6 +78,7 @@ export type AppRuntimeMetadata = {
77
78
  };
78
79
 
79
80
  export type AppRegistryEntry = {
81
+ access?: AppAccessRule;
80
82
  platformPermissions?: readonly PlatformPermission[];
81
83
  id: string;
82
84
  name: string;
@@ -0,0 +1,68 @@
1
+ import type { SQL } from "bun";
2
+ import { z } from "zod";
3
+ import { logger } from "../services/logging";
4
+ import { superviseRuntimeTask } from "../services/runtime-lifecycle";
5
+ import { defineLivePublisher, liveTopic } from "./live-delivery";
6
+
7
+ const change = z.object({ type: z.literal("app.access.changed"), appId: z.string() });
8
+ // Core already owns and publishes this topic and outbox; no second delivery mechanism.
9
+ const changes = defineLivePublisher({ appId: "core", event: change });
10
+
11
+ /** Call in the grant/state mutation transaction; rolled-back changes publish nothing. */
12
+ export const publishAppAccessChanged = (tx: SQL, appId: string): Promise<void> =>
13
+ changes.publish(tx, { key: `app.access:${appId}`, data: { type: "app.access.changed", appId }, access: true });
14
+ export const wakeAppAccessChanged = (): void => changes.wake();
15
+
16
+ const listeners = new Map<string, Set<() => void>>();
17
+ let active: AbortController | undefined;
18
+
19
+ /** One follower per process, released as soon as the last live connection ends. */
20
+ export const subscribeAppAccessChanged = (appId: string, changed: () => void): (() => void) => {
21
+ let callbacks = listeners.get(appId);
22
+ if (!callbacks) {
23
+ callbacks = new Set();
24
+ listeners.set(appId, callbacks);
25
+ }
26
+ callbacks.add(changed);
27
+ if (!active) {
28
+ const controller = new AbortController();
29
+ active = controller;
30
+ const notifyAll = () => {
31
+ for (const callbacks of listeners.values()) for (const callback of callbacks) callback();
32
+ };
33
+ void superviseRuntimeTask({
34
+ name: "App access invalidations",
35
+ signal: controller.signal,
36
+ run: async (signal) => {
37
+ const source = liveTopic("core");
38
+ const after = await source.head();
39
+ if (signal.aborted) return;
40
+ // Recheck after capturing the cursor to cover changes during subscription startup/reconnect.
41
+ notifyAll();
42
+ for await (const event of source.follow({ after, signal })) {
43
+ const parsed = change.safeParse(event.data.d);
44
+ if (parsed.success) for (const callback of listeners.get(parsed.data.appId) ?? []) callback();
45
+ }
46
+ },
47
+ onError: ({ error }) => {
48
+ notifyAll();
49
+ logger("app-access").warn("App access invalidations failed; reconnecting", {
50
+ error: error instanceof Error ? error.message : String(error),
51
+ });
52
+ },
53
+ }).finally(() => {
54
+ if (active === controller) active = undefined;
55
+ });
56
+ }
57
+ let subscribed = true;
58
+ return () => {
59
+ if (!subscribed) return;
60
+ subscribed = false;
61
+ callbacks.delete(changed);
62
+ if (callbacks.size === 0) listeners.delete(appId);
63
+ if (listeners.size === 0) {
64
+ active?.abort();
65
+ active = undefined;
66
+ }
67
+ };
68
+ };
@@ -0,0 +1,128 @@
1
+ import type { Topic, TopicConfig } from "@k2b/sync";
2
+ import { type SQL, sql } from "bun";
3
+ import type { z } from "zod";
4
+ import { lazySync } from "../_internal/process-sync";
5
+ import { logger } from "../services/logging";
6
+ import { createPgOutbox } from "../services/outbox";
7
+ import type { LiveEnvelope } from "./live-engine";
8
+
9
+ const RECONCILE_INTERVAL_MS = 1_000;
10
+ const STALE_CHECK_INTERVAL_MS = 60_000;
11
+
12
+ /**
13
+ * The live topic of one application. Frozen: Sync rejects a declaration that
14
+ * differs from the existing stream, so two releases with different values
15
+ * would break each other during a rollout. A change ships under a new id.
16
+ */
17
+ export const liveTopicConfig = (appId: string) =>
18
+ ({
19
+ id: `cloud:live:${appId}`,
20
+ owner: "cloud",
21
+ retention: { maxAgeMs: 24 * 3_600_000, maxBytes: 64 * 1024 ** 2 },
22
+ maxPayloadBytes: 40 * 1024,
23
+ deadLetterRetention: { maxBytes: 1024 ** 2 },
24
+ }) satisfies TopicConfig;
25
+
26
+ type LiveOutboxRow = { id: string; attempts: number; ordering_key: string; payload: LiveEnvelope };
27
+
28
+ const log = logger("events:live");
29
+
30
+ const liveTopics = lazySync((sync) => {
31
+ const topics = new Map<string, Topic<LiveEnvelope>>();
32
+ return (appId: string): Topic<LiveEnvelope> => {
33
+ const existing = topics.get(appId);
34
+ if (existing) return existing;
35
+ const topic = sync.topic<LiveEnvelope>(liveTopicConfig(appId));
36
+ topics.set(appId, topic);
37
+ return topic;
38
+ };
39
+ });
40
+
41
+ /** Dispatcher of one application's live rows: ordered per key, deleted once published. */
42
+ export const liveOutbox = (appId: string, publish: (row: LiveOutboxRow) => Promise<unknown>) =>
43
+ createPgOutbox<LiveOutboxRow>({
44
+ table: "events.outbox",
45
+ name: `events:live:${appId}`,
46
+ where: { kind: "live", app_id: appId },
47
+ orderBy: "ordering_key",
48
+ sequence: "seq",
49
+ reconcileIntervalMs: RECONCILE_INTERVAL_MS,
50
+ publish,
51
+ });
52
+
53
+ /** Applications whose live updates this process defines, with the wake of their running dispatcher. */
54
+ const dispatchers = new Map<string, (() => void) | null>();
55
+
56
+ export const liveTopic = (appId: string): Topic<LiveEnvelope> => liveTopics()(appId);
57
+
58
+ const logStaleRows = async (appId: string): Promise<void> => {
59
+ const [row] = await sql<{ count: number; oldest_seconds: number | null }[]>`
60
+ SELECT COUNT(*)::int AS count, EXTRACT(EPOCH FROM now() - MIN(created_at))::int AS oldest_seconds
61
+ FROM events.outbox
62
+ WHERE kind = 'live' AND app_id = ${appId} AND created_at < now() - interval '60 seconds'
63
+ `;
64
+ if (row && row.count > 0) log.warn("Live updates wait to be published", { appId, count: row.count, oldestSeconds: row.oldest_seconds });
65
+ };
66
+
67
+ /** Starts delivery for an application with registered publishers; shared with live sockets and access invalidations. */
68
+ export const startLiveDelivery = async (appId: string): Promise<(() => Promise<void>) | null> => {
69
+ if (!dispatchers.has(appId)) return null;
70
+ const [installed] = await sql<{ ready: boolean }[]>`
71
+ SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
72
+ `;
73
+ if (!installed?.ready) {
74
+ throw new Error(
75
+ `"${appId}" writes live updates to events.outbox, which does not exist. Update Cloud Core first: its migration creates the outbox.`,
76
+ );
77
+ }
78
+ const topic = liveTopics()(appId);
79
+ const outbox = liveOutbox(appId, (row) => topic.publish({ data: row.payload, orderingKey: row.ordering_key, idempotencyKey: row.id }));
80
+ outbox.start();
81
+ dispatchers.set(appId, () => void outbox.notify());
82
+ const staleCheck = setInterval(() => {
83
+ logStaleRows(appId).catch((error) =>
84
+ log.warn("Live outbox check failed", { appId, error: error instanceof Error ? error.message : String(error) }),
85
+ );
86
+ }, STALE_CHECK_INTERVAL_MS);
87
+ staleCheck.unref();
88
+ return async () => {
89
+ clearInterval(staleCheck);
90
+ dispatchers.set(appId, null);
91
+ await outbox.stop();
92
+ };
93
+ };
94
+
95
+ /** Transactional publisher using the application's existing live topic and outbox. */
96
+ export const defineLivePublisher = <const Event extends z.ZodType>(definition: { appId: string; event: Event }) => {
97
+ const { appId, event } = definition;
98
+ if (!dispatchers.has(appId)) dispatchers.set(appId, null);
99
+ return {
100
+ /**
101
+ * Writes one update in `tx`, the transaction that makes the change: a
102
+ * rollback writes nothing, a commit publishes it at least once. Data above
103
+ * 32 KiB becomes a reload hint for `key`; `data` must not hold anything a
104
+ * reader of `key` may not see. `access: true` announces that who may read
105
+ * `key` changed, in a row of its own before the data: every replica checks
106
+ * its subscribers of `key` again before it delivers the data, and its
107
+ * collections right after.
108
+ */
109
+ publish: async (
110
+ tx: SQL,
111
+ input: { key: string; data: z.input<Event>; access?: true } | { key: string; data?: undefined; access: true },
112
+ ): Promise<void> => {
113
+ // Store the JSON form of the input, and validate exactly that form here:
114
+ // the browser parses it, and data that does not survive JSON fails this write.
115
+ const data: unknown = input.data === undefined ? undefined : JSON.parse(JSON.stringify(input.data));
116
+ if (data !== undefined) event.parse(data);
117
+ const enqueue = (envelope: LiveEnvelope) =>
118
+ tx`SELECT events.enqueue(${crypto.randomUUID()}::uuid, ${appId}, 'live', ${input.key}, ${JSON.stringify(envelope)}::text::jsonb)`;
119
+ // The access change is a row of its own, so data too large for the outbox cannot drop it.
120
+ if (input.access) await enqueue({ v: 1, k: input.key, a: true });
121
+ if (data !== undefined) await enqueue({ v: 1, k: input.key, d: data });
122
+ },
123
+ /** Publishes committed updates now instead of within the next second. Call it after the commit. */
124
+ wake: (): void => dispatchers.get(appId)?.(),
125
+ /** The topic head. Read it before loading the snapshot it belongs to. */
126
+ cursor: (): Promise<string> => liveTopics()(appId).head(),
127
+ };
128
+ };