@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,92 @@
1
+ import type { TopicDeadLetter, TopicInventoryEntry } from "@k2b/sync";
2
+ import { type SQL, sql } from "bun";
3
+ import { lazySync } from "../_internal/process-sync";
4
+ import { getCapability, listCapabilities } from "../_internal/registry";
5
+ import type { AppEventEnvelope } from "../contracts/app-events";
6
+ import type { CapabilityRegistryEntry } from "../contracts/registry";
7
+ import { appEventTopic } from "../events/app-event-delivery";
8
+ import { createEventAcceptance } from "./app-events/accept";
9
+ import { createEventAdministration } from "./app-events/admin";
10
+ import { createDeclarationSync } from "./app-events/declarations";
11
+ import { createEventDelivery } from "./app-events/deliver";
12
+ import type { signAppEventInvocation } from "./identity/invocation-token";
13
+ import { logger } from "./logging";
14
+
15
+ export { APP_EVENT_DELIVERY_ATTEMPTS, APP_EVENT_DELIVERY_BACKOFF_MS } from "./app-events/deliver";
16
+
17
+ const log = logger("app-events:broker");
18
+ const topicInventory = lazySync((sync) => () => sync.listTopics({ idPrefix: "cloud:app-events" }));
19
+ type BrokerDependencies = {
20
+ db?: SQL;
21
+ registry?: () => Promise<CapabilityRegistryEntry[]>;
22
+ getApp?: typeof getCapability;
23
+ fetch?: typeof fetch;
24
+ sign?: typeof signAppEventInvocation;
25
+ deadLetters?: (after?: string) => Promise<TopicDeadLetter<AppEventEnvelope>[]>;
26
+ streams?: () => AsyncIterable<Pick<TopicInventoryEntry, "id" | "firstSequence">>;
27
+ };
28
+ const message = (error: unknown): string => (error instanceof Error ? error.message : String(error)).slice(0, 1000);
29
+
30
+ export const createAppEventBroker = (dependencies: BrokerDependencies = {}) => {
31
+ const db = dependencies.db ?? sql;
32
+ const registry = dependencies.registry ?? listCapabilities;
33
+ const declarations = createDeclarationSync(db, registry);
34
+ const admin = createEventAdministration(db, {
35
+ registry,
36
+ deadLetters: dependencies.deadLetters ?? ((after) => appEventTopic().deadLetters.list({ limit: 100, after })),
37
+ streams: dependencies.streams ?? (() => topicInventory()()),
38
+ });
39
+ const acceptance = createEventAcceptance(db, declarations.reconcileSubscriptions);
40
+ const delivery = createEventDelivery(db, dependencies.getApp ?? getCapability, declarations.reconcileSubscriptions, dependencies);
41
+ return { ...declarations, ...acceptance, ...delivery, ...admin };
42
+ };
43
+
44
+ export const appEventBroker = createAppEventBroker();
45
+ export const startAppEventBroker = async (): Promise<() => Promise<void>> => {
46
+ const worker = await appEventTopic().process(
47
+ {
48
+ consumer: "core-app-events-v1",
49
+ start: "earliest",
50
+ concurrency: 1,
51
+ recoverDeadLetters: true,
52
+ delivery: { maxAttempts: 8, backoffMs: [1000, 5000, 30000, 120000, 600000, 1800000, 3600000], maxInFlight: 1 },
53
+ },
54
+ async (event) => {
55
+ try {
56
+ await appEventBroker.accept(event.data, event.sequence, event.attempt);
57
+ } catch (error) {
58
+ if (event.attempt >= 8) {
59
+ await appEventBroker.markSeen(event.sequence);
60
+ }
61
+ throw error;
62
+ }
63
+ },
64
+ );
65
+ let stopping = false;
66
+ let active: Promise<void> | null = null;
67
+ let tickCount = 0;
68
+ const run = () => {
69
+ if (stopping || active) return;
70
+ active = (async () => {
71
+ await appEventBroker.work();
72
+ if (++tickCount % 60 === 0) {
73
+ await appEventBroker.checkGap();
74
+ await appEventBroker.retain();
75
+ }
76
+ })()
77
+ .catch((error) => log.error("App event broker pass failed", { error: message(error) }))
78
+ .finally(() => {
79
+ active = null;
80
+ });
81
+ };
82
+ const timer = setInterval(run, 1000);
83
+ timer.unref();
84
+ run();
85
+ return async () => {
86
+ stopping = true;
87
+ clearInterval(timer);
88
+ worker.stop();
89
+ await worker.drain();
90
+ await active;
91
+ };
92
+ };
@@ -0,0 +1,11 @@
1
+ export class ConfigConflict extends Error {
2
+ readonly code = "conflict";
3
+ constructor(
4
+ readonly current: unknown,
5
+ readonly changedBy: { id: string; name: string } | null,
6
+ readonly changedAt: string,
7
+ readonly key?: string,
8
+ ) {
9
+ super("This configuration changed. Read the current version before saving.");
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export { coreUserEvents, coreUserEventTypes } from "../events/core-user-created";
@@ -1,6 +1,8 @@
1
1
  import { X509Certificate } from "node:crypto";
2
+ import type { SQL } from "bun";
2
3
  import { setFreeIpaTlsResolver } from "../server/services/freeipa/tls";
3
4
  import { coreSettings } from "./settings/api";
5
+ import { readKeyState, SettingUnreadableError } from "./settings/store";
4
6
 
5
7
  const MAX_CA_CERT_BYTES = 256 * 1024;
6
8
  const PEM_CERTIFICATE_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
@@ -82,6 +84,13 @@ const normalizeStringList = (value: unknown, fallback: string[] = []): string[]
82
84
  return normalized.length > 0 ? normalized : fallback;
83
85
  };
84
86
 
87
+ /** Read the role mapping on the caller's connection during authorization. */
88
+ export const getFreeIpaAdminGroups = async (db: SQL): Promise<string[]> => {
89
+ const state = await readKeyState("freeipa.groups.admin", db);
90
+ if (state.stored === "unreadable") throw new SettingUnreadableError("freeipa.groups.admin");
91
+ return normalizeStringList(state.value, ["admins"]);
92
+ };
93
+
85
94
  /**
86
95
  * Read the full FreeIPA config snapshot from settings (Redis cache-aside +
87
96
  * Postgres fallback). Always returns within Redis-TTL fresh data — no hidden
@@ -1,4 +1,5 @@
1
1
  import { type SQL, sql } from "bun";
2
+ import type { AppPwaActor } from "../../app-pwa/contracts";
2
3
  import type { AccessSubject, CapabilityExecutionContext } from "../../contracts";
3
4
  import { audit } from "../audit";
4
5
  import { GeoError } from "./errors";
@@ -102,7 +103,7 @@ export const createGeoLedger = (db: SQL = sql) => ({
102
103
  async recordTransition(
103
104
  event: "provider_key_rejected" | "monthly_budget_exhausted" | "route_cost_overrun",
104
105
  timezone: string,
105
- context: Pick<CapabilityExecutionContext, "actor"> & Partial<Pick<CapabilityExecutionContext, "requestId">>,
106
+ context: { actor: CapabilityExecutionContext["actor"] | AppPwaActor; requestId?: string },
106
107
  now: Date,
107
108
  metadata: Record<string, number> = {},
108
109
  ) {
@@ -114,21 +115,38 @@ export const createGeoLedger = (db: SQL = sql) => ({
114
115
  : await tx`INSERT INTO geo.audit_transitions(event,period) VALUES(${event},${period}) ON CONFLICT DO NOTHING RETURNING event`;
115
116
  if (!inserted.length) return;
116
117
  const actor =
117
- context.actor.kind === "user"
118
- ? {
119
- userId: context.actor.user.id,
120
- uid: context.actor.user.uid,
121
- provider: context.actor.user.provider,
122
- roles: context.actor.user.roles,
123
- }
124
- : { uid: `service-account:${context.actor.serviceAccount.id}`, provider: "service_account", roles: context.actor.scopes };
118
+ context.actor.kind === "device" || context.actor.kind === "visit"
119
+ ? null
120
+ : context.actor.kind === "user"
121
+ ? {
122
+ userId: context.actor.user.id,
123
+ uid: context.actor.user.uid,
124
+ provider: context.actor.user.provider,
125
+ roles: context.actor.user.roles,
126
+ }
127
+ : { uid: `service-account:${context.actor.serviceAccount.id}`, provider: "service_account", roles: context.actor.scopes };
125
128
  await audit.record(
126
129
  {
127
130
  action: `core.geo.${event}`,
128
131
  outcome: event === "monthly_budget_exhausted" ? "denied" : "failed",
129
132
  actor,
130
133
  requestId: context.requestId,
131
- metadata: { period, ...metadata },
134
+ metadata: {
135
+ period,
136
+ ...metadata,
137
+ ...(context.actor.kind === "device" || context.actor.kind === "visit"
138
+ ? {
139
+ appId: context.actor.appId,
140
+ subject: { type: "user", userId: context.actor.ownerUserId },
141
+ via: {
142
+ kind: context.actor.kind,
143
+ appId: context.actor.appId,
144
+ deviceId: context.actor.deviceId,
145
+ ...(context.actor.kind === "visit" ? { visitId: context.actor.visitId } : {}),
146
+ },
147
+ }
148
+ : {}),
149
+ },
132
150
  },
133
151
  tx,
134
152
  );
@@ -1,4 +1,5 @@
1
1
  import type { z } from "zod";
2
+ import type { AppPwaActor } from "../../app-pwa/contracts";
2
3
  import type { AccessSubject, CapabilityExecutionContext, RequestActor } from "../../contracts";
3
4
  import {
4
5
  GeoAutocompleteDataSchema,
@@ -26,17 +27,27 @@ import { createGeoRouteService } from "./route-service";
26
27
  import { type GeoConfig, geoPlan, readGeoConfig } from "./settings";
27
28
 
28
29
  export type GeoContext = Partial<Pick<CapabilityExecutionContext, "actor" | "accessSubject" | "locale" | "signal" | "requestId">>;
30
+ /** Internal, narrow App-PWA autocomplete adapter; never a RequestActor or capability caller. */
31
+ export type AppPwaGeoContext = { actor: AppPwaActor; locale?: string; signal?: AbortSignal; requestId?: string };
32
+ type GeoCallContext = GeoContext | AppPwaGeoContext;
29
33
  export const geoIdentity = (context: GeoContext): { actor: RequestActor; accessSubject: AccessSubject } => {
30
34
  const { actor, accessSubject } = context;
31
35
  if (
32
36
  !actor ||
33
37
  !accessSubject ||
34
- !(actor.kind === "user" ? actor.user?.id : actor.serviceAccount?.id) ||
38
+ !(actor.kind === "user" ? actor.user?.id : actor.kind === "service_account" ? actor.serviceAccount?.id : undefined) ||
35
39
  !(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
36
40
  )
37
41
  throw new GeoError("GEO_FORBIDDEN");
38
42
  return { actor, accessSubject };
39
43
  };
44
+ const geoCallIdentity = (context: GeoCallContext): { actor: RequestActor | AppPwaActor; accessSubject: AccessSubject } => {
45
+ if (context.actor?.kind === "device" || context.actor?.kind === "visit") {
46
+ if (!context.actor.ownerUserId) throw new GeoError("GEO_FORBIDDEN");
47
+ return { actor: context.actor, accessSubject: { type: "user", userId: context.actor.ownerUserId } };
48
+ }
49
+ return geoIdentity({ actor: context.actor, accessSubject: "accessSubject" in context ? context.accessSubject : undefined });
50
+ };
40
51
  const bound = (name: string, value: number, min: number, max: number, integer = false) => {
41
52
  if (!Number.isFinite(value) || value > max || value < min || (integer && !Number.isInteger(value)))
42
53
  limitExceeded(name, value < min ? min : max, value);
@@ -84,10 +95,10 @@ export const createGeoService = (
84
95
  const recordTransition = async (
85
96
  event: "monthly_budget_exhausted" | "provider_key_rejected" | "route_cost_overrun",
86
97
  timezone: string,
87
- context: GeoContext,
98
+ context: GeoCallContext,
88
99
  metadata?: Record<string, number>,
89
100
  ) => {
90
- const identity = geoIdentity(context);
101
+ const identity = geoCallIdentity(context);
91
102
  try {
92
103
  await ledger.recordTransition(event, timezone, { ...context, ...identity }, now(), metadata);
93
104
  return true;
@@ -107,7 +118,7 @@ export const createGeoService = (
107
118
  const paid = async <T>(
108
119
  fn: GeoFunction,
109
120
  input: unknown,
110
- context: GeoContext,
121
+ context: GeoCallContext,
111
122
  settings: GeoConfig,
112
123
  schema: z.ZodType<T>,
113
124
  request: (signal: AbortSignal) => Promise<T>,
@@ -115,7 +126,7 @@ export const createGeoService = (
115
126
  cacheInput = input,
116
127
  empty: (value: T) => boolean = () => false,
117
128
  ): Promise<T> => {
118
- const identity = geoIdentity(context);
129
+ const identity = geoCallIdentity(context);
119
130
  const locale = context.locale ?? "en";
120
131
  const key = geoCacheKey(fn, cacheInput, locale);
121
132
  const hit = await cache.get(key, schema);
@@ -176,29 +187,37 @@ export const createGeoService = (
176
187
  await release();
177
188
  }
178
189
  };
190
+ const autocomplete = async (input: GeoAutocompleteInput, context: GeoCallContext) => {
191
+ geoCallIdentity(context);
192
+ const settings = await config();
193
+ if (!settings.apiKey.trim()) throw new GeoError("GEO_NOT_CONFIGURED");
194
+ const value = parse(GeoAutocompleteInputSchema, input);
195
+ bound("text", value.text.length, 3, 300);
196
+ bound("text", value.text.trim().length, 3, 300);
197
+ bound("limit", value.limit ?? 5, 1, 5, true);
198
+ country(value.countryCode);
199
+ area(value.area);
200
+ return paid(
201
+ "autocomplete",
202
+ value,
203
+ context,
204
+ settings,
205
+ GeoAutocompleteDataSchema,
206
+ (signal) => adapter.autocomplete(value, settings.apiKey, context.locale ?? "en", signal),
207
+ value.limit,
208
+ { ...value, limit: value.limit ?? 5 },
209
+ (result) => !result.suggestions.length,
210
+ );
211
+ };
179
212
  const routes = createGeoRouteService({ configured, adapter, cache, bucket, ledger, now, recordTransition });
180
213
  return {
181
214
  ...routes,
182
- async autocomplete(input: GeoAutocompleteInput, context: GeoContext) {
183
- const settings = await configured(context);
184
- const value = parse(GeoAutocompleteInputSchema, input);
185
- bound("text", value.text.length, 3, 300);
186
- bound("text", value.text.trim().length, 3, 300);
187
- bound("limit", value.limit ?? 5, 1, 5, true);
188
- country(value.countryCode);
189
- area(value.area);
190
- return paid(
191
- "autocomplete",
192
- value,
193
- context,
194
- settings,
195
- GeoAutocompleteDataSchema,
196
- (signal) => adapter.autocomplete(value, settings.apiKey, context.locale ?? "en", signal),
197
- value.limit,
198
- { ...value, limit: value.limit ?? 5 },
199
- (result) => !result.suggestions.length,
200
- );
215
+ autocomplete: async (input: GeoAutocompleteInput, context: GeoContext) => {
216
+ geoIdentity(context);
217
+ return autocomplete(input, context);
201
218
  },
219
+ // Only the App-PWA service calls this after locking the pairing and optional visit authority.
220
+ autocompleteForAppPwa: (input: GeoAutocompleteInput, context: AppPwaGeoContext) => autocomplete(input, context),
202
221
  async geocode(input: GeoGeocodeInput, context: GeoContext) {
203
222
  const settings = await configured(context);
204
223
  const value = parse(GeoGeocodeInputSchema, input);
@@ -0,0 +1,12 @@
1
+ import { env } from "../../config/env";
2
+ import { publicCloudOrigin } from "../../shared/app-url";
3
+ import { get } from "../settings";
4
+
5
+ export const coreOrigin = async (internal = false): Promise<string> => {
6
+ const configured = env.CLOUD_CORE_INTERNAL_ORIGIN;
7
+ if (internal && !configured) throw new Error("CLOUD_CORE_INTERNAL_ORIGIN is required for internal Core requests");
8
+ const origin = configured || publicCloudOrigin(await get<string>("app.url"));
9
+ const url = new URL(origin);
10
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("CLOUD_CORE_INTERNAL_ORIGIN must use http or https");
11
+ return url.origin;
12
+ };
@@ -3,6 +3,7 @@ import type { RequestActor } from "../../server/middleware/auth";
3
3
  import type { AccessSubject } from "../../server/services/access";
4
4
  import { isAccountCategoryAllowed } from "../account-category-policy";
5
5
  import { isAccountExpired } from "../account-model";
6
+ import { isServiceAccountOAuthAuthorityCurrent } from "../oauth-tokens";
6
7
  import type { ServiceAccount } from "../service-accounts";
7
8
  import { buildProjectedUser, loadCurrentUser, userProjectionJoin, userProjectionSql, withoutAdminRole } from "../session/user";
8
9
  import type { CloudInvocationClaims } from "./invocation-token";
@@ -38,6 +39,8 @@ export type InvocationPrincipalClaims = Pick<
38
39
  | "session_kind"
39
40
  | "scopes"
40
41
  | "credential_id"
42
+ | "oauth_client_id"
43
+ | "credential_expires_at"
41
44
  | "exp"
42
45
  | "act"
43
46
  | "credential_kind"
@@ -155,15 +158,30 @@ export const resolveInvocationAuthority = async (
155
158
  )
156
159
  return null;
157
160
 
158
- const actor: RequestActor = {
161
+ const actor: Extract<RequestActor, { kind: "service_account" }> = {
159
162
  kind: "service_account",
160
163
  serviceAccount,
161
164
  delegatedUser,
162
165
  scopes: [...claims.scopes],
163
166
  credentialId: claims.credential_id ?? null,
164
- credentialExpiresAt: new Date(claims.exp * 1_000).toISOString(),
167
+ oauthClientId: claims.oauth_client_id ?? null,
168
+ credentialExpiresAt: new Date(
169
+ Math.min(claims.exp * 1_000, claims.credential_expires_at ? Date.parse(claims.credential_expires_at) : Infinity),
170
+ ).toISOString(),
165
171
  delegation,
166
172
  };
173
+ if (
174
+ claims.oauth_client_id &&
175
+ !(await isServiceAccountOAuthAuthorityCurrent(
176
+ {
177
+ serviceAccountId: serviceAccount.id,
178
+ clientId: claims.oauth_client_id,
179
+ expiresAt: actor.credentialExpiresAt,
180
+ },
181
+ { query, groupsAdmin },
182
+ ))
183
+ )
184
+ return null;
167
185
  const accessSubject: AccessSubject = delegatedUser
168
186
  ? { type: "user", userId: delegatedUser.id, delegatedByServiceAccountId: serviceAccount.id }
169
187
  : { type: "service_account", serviceAccountId: serviceAccount.id };
@@ -35,6 +35,8 @@ export const invocationAuthorityFromRequest = (authority: RequestAuthority): Inv
35
35
  delegated_user_id: actor.delegatedUser.id,
36
36
  credential_kind: authority.credentialKind,
37
37
  ...(actor.credentialId ? { credential_id: actor.credentialId } : {}),
38
+ ...(actor.oauthClientId ? { oauth_client_id: actor.oauthClientId } : {}),
39
+ ...(actor.credentialExpiresAt ? { credential_expires_at: actor.credentialExpiresAt } : {}),
38
40
  scopes: [...new Set(authority.scopes)].sort(),
39
41
  };
40
42
  }
@@ -49,6 +51,8 @@ export const invocationAuthorityFromRequest = (authority: RequestAuthority): Inv
49
51
  access_subject_id: actor.serviceAccount.id,
50
52
  credential_kind: authority.credentialKind,
51
53
  ...(actor.credentialId ? { credential_id: actor.credentialId } : {}),
54
+ ...(actor.oauthClientId ? { oauth_client_id: actor.oauthClientId } : {}),
55
+ ...(actor.credentialExpiresAt ? { credential_expires_at: actor.credentialExpiresAt } : {}),
52
56
  scopes: [...new Set(authority.scopes)].sort(),
53
57
  };
54
58
  };