@k2b/cloud 0.47.0 → 0.49.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 (240) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-manager-sql.ts +10 -0
  3. package/src/_internal/app-access-resources.ts +2 -1
  4. package/src/_internal/app-capabilities.ts +73 -0
  5. package/src/_internal/app-event-catalog.ts +1 -1
  6. package/src/_internal/app-queries.ts +1 -1
  7. package/src/_internal/application-package.ts +2 -1
  8. package/src/_internal/bounded-json.ts +7 -1
  9. package/src/_internal/capabilities.ts +25 -2
  10. package/src/_internal/define-app.ts +33 -2
  11. package/src/_internal/heartbeat.ts +2 -0
  12. package/src/_internal/nats-connection.ts +10 -9
  13. package/src/_internal/process-sync.ts +6 -6
  14. package/src/_internal/public-surface.ts +76 -0
  15. package/src/_internal/purpose-name.ts +7 -0
  16. package/src/_internal/registry-validation.ts +1 -0
  17. package/src/_internal/registry.ts +49 -3
  18. package/src/_internal/workload-credential.ts +2 -1
  19. package/src/ai/app-skill-store.ts +2 -1
  20. package/src/ai/app-skills.ts +2 -1
  21. package/src/ai/approvals.ts +2 -1
  22. package/src/ai/chat-tasks.ts +2 -1
  23. package/src/ai/credentials.ts +2 -1
  24. package/src/ai/dictation-runtime.ts +1 -1
  25. package/src/ai/dictations.ts +1 -1
  26. package/src/ai/files-store.ts +2 -1
  27. package/src/ai/inference-calls.ts +2 -1
  28. package/src/ai/map-store.ts +1 -1
  29. package/src/ai/map-tool.ts +1 -1
  30. package/src/ai/memories.ts +1 -1
  31. package/src/ai/memory-learning-runs.ts +1 -1
  32. package/src/ai/memory-learning.ts +1 -1
  33. package/src/ai/memory-workflow-evidence.ts +1 -1
  34. package/src/ai/message-queue.ts +2 -1
  35. package/src/ai/migrate.ts +1 -1
  36. package/src/ai/model-access-migrate.ts +1 -1
  37. package/src/ai/model-access.ts +1 -1
  38. package/src/ai/model-catalog.ts +1 -1
  39. package/src/ai/model-delete.ts +1 -1
  40. package/src/ai/model-usage.ts +2 -1
  41. package/src/ai/open-questions.ts +1 -1
  42. package/src/ai/openai-api-slots.ts +2 -1
  43. package/src/ai/prefs.ts +2 -1
  44. package/src/ai/projects.ts +2 -1
  45. package/src/ai/provider-connections.ts +1 -1
  46. package/src/ai/quota-limits.ts +2 -1
  47. package/src/ai/quota-provider.ts +2 -1
  48. package/src/ai/quota-report.ts +2 -1
  49. package/src/ai/quotas-migrate.ts +2 -1
  50. package/src/ai/quotas.ts +2 -1
  51. package/src/ai/request-headers.ts +2 -1
  52. package/src/ai/review-content.ts +2 -1
  53. package/src/ai/skill-search.ts +2 -1
  54. package/src/ai/skills.ts +2 -1
  55. package/src/ai/store.ts +2 -1
  56. package/src/ai/structured-runs.ts +2 -1
  57. package/src/ai/tool-audit.ts +2 -1
  58. package/src/ai/turn-timing.ts +1 -1
  59. package/src/ai/usage.ts +2 -1
  60. package/src/ai/web-provenance.ts +2 -1
  61. package/src/api/admin-ai-quotas.ts +1 -1
  62. package/src/api/admin-ai.ts +1 -1
  63. package/src/api/admin-app-domains.ts +62 -0
  64. package/src/api/admin-app-keys.ts +47 -3
  65. package/src/api/admin-core-settings.ts +1 -1
  66. package/src/api/admin-outgoing-mail.ts +1 -1
  67. package/src/api/app-bootstrap.ts +153 -0
  68. package/src/api/app-events.ts +1 -1
  69. package/src/api/auth.ts +1 -1
  70. package/src/api/authenticate-app-request.ts +129 -0
  71. package/src/api/capabilities.ts +87 -6
  72. package/src/api/index.ts +7 -0
  73. package/src/api/me.ts +1 -1
  74. package/src/api/search.bench.ts +1 -1
  75. package/src/app-pwa/service.ts +2 -1
  76. package/src/app-pwa/transaction.ts +4 -2
  77. package/src/capabilities/claims.ts +2 -1
  78. package/src/capabilities/executions.ts +8 -4
  79. package/src/capabilities/geo-client.ts +1 -1
  80. package/src/capabilities/migrate.ts +3 -1
  81. package/src/capabilities/resource-readers.ts +2 -2
  82. package/src/capabilities/response.ts +2 -1
  83. package/src/capabilities/server-invoke.ts +73 -20
  84. package/src/capabilities/server.ts +2 -1
  85. package/src/cli/admin/app-domains.ts +140 -0
  86. package/src/cli/admin/app-keys.ts +25 -3
  87. package/src/cli/admin/index.ts +107 -88
  88. package/src/config/env.ts +10 -4
  89. package/src/contracts/app-domains.ts +91 -0
  90. package/src/contracts/app-status.ts +104 -0
  91. package/src/contracts/capabilities.ts +37 -2
  92. package/src/contracts/index.ts +1 -0
  93. package/src/contracts/outgoing-mail.ts +1 -1
  94. package/src/contracts/registry.ts +2 -0
  95. package/src/events/app-event-delivery.ts +1 -1
  96. package/src/events/live-delivery.ts +2 -1
  97. package/src/index.ts +1 -0
  98. package/src/runtime/bootstrap-client.ts +195 -0
  99. package/src/runtime/bootstrap-protocol.ts +67 -0
  100. package/src/runtime/clients.ts +6 -2
  101. package/src/runtime/connections.ts +9 -0
  102. package/src/server/app-key.ts +14 -0
  103. package/src/server/index.ts +2 -0
  104. package/src/server/ratelimit.ts +1 -1
  105. package/src/server/services/access.ts +2 -1
  106. package/src/services/account-category-policy.ts +1 -1
  107. package/src/services/account-lifecycle/audit.ts +1 -1
  108. package/src/services/account-lifecycle/index.ts +1 -1
  109. package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
  110. package/src/services/accounts/app.ts +1 -1
  111. package/src/services/accounts/avatar.ts +1 -1
  112. package/src/services/accounts/base-group.ts +1 -1
  113. package/src/services/accounts/directory-shares.ts +1 -1
  114. package/src/services/accounts/duplicate-emails.ts +1 -1
  115. package/src/services/accounts/entities.ts +2 -1
  116. package/src/services/accounts/group-sql.ts +2 -1
  117. package/src/services/accounts/groups.ts +1 -1
  118. package/src/services/accounts/identities.ts +13 -11
  119. package/src/services/accounts/ipa-data.ts +1 -1
  120. package/src/services/accounts/local-groups.ts +2 -1
  121. package/src/services/accounts/machine-group-management.ts +2 -1
  122. package/src/services/accounts/posix.ts +13 -12
  123. package/src/services/accounts/request-policy.ts +2 -1
  124. package/src/services/accounts/switching.ts +1 -1
  125. package/src/services/accounts/users.ts +2 -1
  126. package/src/services/announcements/index.ts +1 -1
  127. package/src/services/app-access-management.ts +3 -9
  128. package/src/services/app-access.ts +2 -1
  129. package/src/services/app-admin-facts.ts +160 -0
  130. package/src/services/app-approval.ts +34 -30
  131. package/src/services/app-bootstrap.ts +217 -0
  132. package/src/services/app-details.ts +3 -2
  133. package/src/services/app-domain-check.ts +152 -0
  134. package/src/services/app-domains.ts +172 -0
  135. package/src/services/app-events/accept.ts +5 -5
  136. package/src/services/app-events/admin.ts +15 -15
  137. package/src/services/app-events/declarations.ts +6 -3
  138. package/src/services/app-events/deliver.ts +6 -6
  139. package/src/services/app-events.ts +3 -2
  140. package/src/services/app-keys.ts +45 -11
  141. package/src/services/approvals/content.ts +1 -1
  142. package/src/services/approvals/index.ts +2 -1
  143. package/src/services/approvals/list.ts +1 -1
  144. package/src/services/approvals/migrate.ts +1 -1
  145. package/src/services/audit/index.ts +2 -1
  146. package/src/services/auth-flows/ipa.ts +1 -1
  147. package/src/services/auth-flows/magic-link.ts +1 -1
  148. package/src/services/auth-flows/password-reset.ts +1 -1
  149. package/src/services/auth-flows/proxy-return.ts +1 -1
  150. package/src/services/downstream-budget.ts +2 -2
  151. package/src/services/geo/admin.ts +13 -12
  152. package/src/services/geo/identity.ts +46 -0
  153. package/src/services/geo/ledger.ts +118 -106
  154. package/src/services/geo/migrate.ts +2 -1
  155. package/src/services/geo/route-chain.ts +3 -3
  156. package/src/services/geo/service.ts +4 -24
  157. package/src/services/geoip/index.ts +5 -5
  158. package/src/services/geoip/store.ts +24 -21
  159. package/src/services/help/index.ts +2 -1
  160. package/src/services/help/store.ts +2 -1
  161. package/src/services/identity/app-request.ts +44 -0
  162. package/src/services/identity/constants.ts +1 -0
  163. package/src/services/identity/index.ts +1 -0
  164. package/src/services/identity/invocation-actor.ts +2 -1
  165. package/src/services/identity/invocation-token.ts +112 -0
  166. package/src/services/identity/key-ring.ts +2 -2
  167. package/src/services/identity/resource-readers.ts +1 -1
  168. package/src/services/identity/routes.ts +11 -3
  169. package/src/services/identity/runtime-config.ts +14 -3
  170. package/src/services/identity/workload-auth.ts +2 -0
  171. package/src/services/identity/workload-runtime.ts +3 -3
  172. package/src/services/index.ts +4 -0
  173. package/src/services/ipa/groups.ts +1 -1
  174. package/src/services/ipa/profile.ts +1 -1
  175. package/src/services/ipa/search.ts +1 -1
  176. package/src/services/ipa/sync.ts +1 -1
  177. package/src/services/ipa/users.ts +1 -1
  178. package/src/services/known-browsers.ts +2 -1
  179. package/src/services/legal-consent.ts +1 -1
  180. package/src/services/links/index.ts +16 -0
  181. package/src/services/links/runtime.ts +44 -0
  182. package/src/services/links/store.ts +105 -0
  183. package/src/services/logging/index.ts +13 -8
  184. package/src/services/logging/redaction.ts +6 -1
  185. package/src/services/logging/trace.ts +9 -9
  186. package/src/services/logging/web-vitals.ts +1 -1
  187. package/src/services/mandates/index.ts +2 -1
  188. package/src/services/notifications/batches.ts +1 -1
  189. package/src/services/notifications/browser.ts +1 -1
  190. package/src/services/notifications/catalog.ts +1 -1
  191. package/src/services/notifications/dispatcher.ts +1 -1
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/observability.ts +2 -1
  194. package/src/services/notifications/platform.ts +1 -1
  195. package/src/services/notifications/quiet.ts +1 -1
  196. package/src/services/notifications/user.ts +1 -1
  197. package/src/services/oauth-tokens.ts +1 -1
  198. package/src/services/outbox.ts +15 -14
  199. package/src/services/outgoing-mail/admin.ts +1 -1
  200. package/src/services/outgoing-mail/dispatcher.ts +1 -1
  201. package/src/services/outgoing-mail/drain.ts +2 -1
  202. package/src/services/outgoing-mail/enqueue.ts +2 -1
  203. package/src/services/outgoing-mail/messages.ts +2 -1
  204. package/src/services/outgoing-mail/retention.ts +1 -1
  205. package/src/services/outgoing-mail/send.ts +1 -1
  206. package/src/services/outgoing-mail/store.ts +2 -1
  207. package/src/services/panel-context-consent.ts +32 -28
  208. package/src/services/providers/local/auth.ts +1 -1
  209. package/src/services/providers/local/users.ts +1 -1
  210. package/src/services/pwa-devices.ts +2 -1
  211. package/src/services/rail-preferences.ts +16 -12
  212. package/src/services/rail-shortcuts.ts +12 -10
  213. package/src/services/request-cache-redis.ts +3 -3
  214. package/src/services/service-account-credentials.ts +2 -1
  215. package/src/services/service-accounts.ts +2 -1
  216. package/src/services/session/index.ts +2 -1
  217. package/src/services/session/user.ts +2 -1
  218. package/src/services/settings/core-settings.ts +20 -0
  219. package/src/services/settings/crypto.ts +2 -2
  220. package/src/services/settings/index.ts +1 -1
  221. package/src/services/settings/store.ts +6 -1
  222. package/src/services/settings/versions.ts +2 -1
  223. package/src/services/weather/forecast.ts +2 -1
  224. package/src/services/weather/locations.ts +1 -1
  225. package/src/services/weather/migrate.ts +2 -1
  226. package/src/services/weather/short-id.ts +2 -1
  227. package/src/services/webauthn.ts +1 -1
  228. package/src/ssr/admin-navigation.ts +0 -1
  229. package/src/ssr/compact-shell.browser-harness.ts +2 -0
  230. package/src/ssr/platform-messages.ts +0 -2
  231. package/src/start.ts +35 -14
  232. package/src/workflows/ai/store.ts +2 -1
  233. package/src/workflows/store/budget.ts +2 -1
  234. package/src/workflows/store/definitions.ts +2 -1
  235. package/src/workflows/store/events.ts +2 -1
  236. package/src/workflows/store/observability.ts +2 -1
  237. package/src/workflows/store/retention.ts +2 -1
  238. package/src/workflows/store/runs.ts +2 -1
  239. package/src/workflows/store/transaction.ts +2 -1
  240. package/src/workflows/store/worker.ts +2 -1
@@ -1,8 +1,10 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { AppPwaActor } from "../../app-pwa/contracts";
3
- import type { AccessSubject, CapabilityExecutionContext } from "../../contracts";
3
+ import type { CapabilityAppExecutionContext, CapabilityExecutionContext } from "../../contracts";
4
+ import { sql } from "../../runtime/clients";
4
5
  import { audit } from "../audit";
5
6
  import { GeoError } from "./errors";
7
+ import type { GeoBudgetSubject } from "./identity";
6
8
  import type { GeoConfig } from "./settings";
7
9
 
8
10
  export type GeoBudgetConfig = Pick<GeoConfig, "timezone" | "monthlyCredits" | "personDailyCredits">;
@@ -30,126 +32,136 @@ export const geoPeriods = (now: Date, timezone: string): { person: GeoPeriod; cl
30
32
  cloud: { period: day.slice(0, 7), resetsAt: boundary(32 * 86400_000, 7) },
31
33
  };
32
34
  };
33
- export const geoSubjectKey = (subject: AccessSubject) =>
34
- subject.type === "user" ? `user:${subject.userId}` : `service_account:${subject.serviceAccountId}`;
35
+ export const geoSubjectKey = (subject: GeoBudgetSubject) =>
36
+ subject.type === "app"
37
+ ? `app:${subject.appId}`
38
+ : subject.type === "user"
39
+ ? `user:${subject.userId}`
40
+ : `service_account:${subject.serviceAccountId}`;
35
41
  export type GeoReservation = {
36
42
  cost: number;
37
43
  group: string;
38
44
  rows: Array<{ scope: "cloud" | "person"; subject: string; period: string; resetsAt: string; limit: number }>;
39
45
  };
40
- const rowsFor = (subject: AccessSubject, config: GeoBudgetConfig, now: Date) => {
46
+ const rowsFor = (subject: GeoBudgetSubject, config: GeoBudgetConfig, now: Date) => {
41
47
  const periods = geoPeriods(now, config.timezone);
42
48
  return [
43
49
  { scope: "cloud", subject: "*", ...periods.cloud, limit: config.monthlyCredits * 1000 },
44
50
  { scope: "person", subject: geoSubjectKey(subject), ...periods.person, limit: config.personDailyCredits * 1000 },
45
51
  ] satisfies GeoReservation["rows"];
46
52
  };
47
- export const createGeoLedger = (db: SQL = sql) => ({
48
- async reserve(subject: AccessSubject, config: GeoBudgetConfig, cost: number, group: string, now: Date): Promise<GeoReservation> {
49
- if (!Number.isSafeInteger(cost) || cost < 0) throw new Error("Invalid geo reservation");
50
- const rows = rowsFor(subject, config, now);
51
- await db.begin(async (tx) => {
52
- // Fixed order across upserts, updates, shares and settlement: Cloud first, person second.
53
- for (const row of rows)
54
- await tx`INSERT INTO geo.usage(scope,subject,period) VALUES(${row.scope},${row.subject},${row.period}) ON CONFLICT DO NOTHING`;
55
- for (const row of rows) {
56
- // Keep the refusal's available/shortfall numbers in the same row-lock snapshot as admission.
57
- const [balance] = await tx<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
53
+ export const createGeoLedger = (injectedDb?: SQL) => {
54
+ const db = () => injectedDb ?? sql;
55
+ return {
56
+ async reserve(subject: GeoBudgetSubject, config: GeoBudgetConfig, cost: number, group: string, now: Date): Promise<GeoReservation> {
57
+ if (!Number.isSafeInteger(cost) || cost < 0) throw new Error("Invalid geo reservation");
58
+ const rows = rowsFor(subject, config, now);
59
+ await db().begin(async (tx) => {
60
+ // Fixed order across upserts, updates, shares and settlement: Cloud first, person second.
61
+ for (const row of rows)
62
+ await tx`INSERT INTO geo.usage(scope,subject,period) VALUES(${row.scope},${row.subject},${row.period}) ON CONFLICT DO NOTHING`;
63
+ for (const row of rows) {
64
+ // Keep the refusal's available/shortfall numbers in the same row-lock snapshot as admission.
65
+ const [balance] = await tx<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
58
66
  WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period} FOR UPDATE`;
59
- const changed = await tx`UPDATE geo.usage SET used=used+${cost} WHERE scope=${row.scope} AND subject=${row.subject}
67
+ const changed = await tx`UPDATE geo.usage SET used=used+${cost} WHERE scope=${row.scope} AND subject=${row.subject}
60
68
  AND period=${row.period} AND used+${cost}<=${row.limit} RETURNING used`;
61
- if (!changed.length) {
62
- const available = row.limit - (balance?.used ?? 0);
63
- throw new GeoError("GEO_BUDGET_EXHAUSTED", {
64
- scope: row.scope,
65
- resetsAt: row.resetsAt,
66
- requiredCredits: cost / 1000,
67
- availableCredits: available / 1000,
68
- shortfallCredits: (cost - available) / 1000,
69
- });
69
+ if (!changed.length) {
70
+ const available = row.limit - (balance?.used ?? 0);
71
+ throw new GeoError("GEO_BUDGET_EXHAUSTED", {
72
+ scope: row.scope,
73
+ resetsAt: row.resetsAt,
74
+ requiredCredits: cost / 1000,
75
+ availableCredits: available / 1000,
76
+ shortfallCredits: (cost - available) / 1000,
77
+ });
78
+ }
70
79
  }
71
- }
72
- for (const row of rows)
73
- await tx`INSERT INTO geo.shares(scope,subject,period,function_group,used)
80
+ for (const row of rows)
81
+ await tx`INSERT INTO geo.shares(scope,subject,period,function_group,used)
74
82
  VALUES(${row.scope},${row.subject},${row.period},${group},${cost})
75
83
  ON CONFLICT(scope,subject,period,function_group) DO UPDATE SET used=geo.shares.used+EXCLUDED.used`;
76
- });
77
- return { cost, group, rows };
78
- },
79
- async settle(reservation: GeoReservation, referenceCost: number) {
80
- const refund = reservation.cost - referenceCost;
81
- if (!Number.isSafeInteger(referenceCost) || referenceCost < 0 || !Number.isSafeInteger(refund))
82
- throw new Error("Invalid geo settlement");
83
- if (!refund) return;
84
- // Negative refunds truthfully debit known routing overruns. Admission still uses the original
85
- // bounded CAS transaction; future reservations see the overdrawn counters and are refused.
86
- // Settle once per reservation, in the same fixed row order as admission.
87
- await db.begin(async (tx) => {
88
- for (const row of reservation.rows) {
89
- await tx`UPDATE geo.usage SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period}`;
90
- await tx`UPDATE geo.shares SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period} AND function_group=${reservation.group}`;
91
- }
92
- });
93
- },
94
- async availability(subject: AccessSubject, config: GeoBudgetConfig, now: Date) {
95
- const balances = [];
96
- for (const row of rowsFor(subject, config, now)) {
97
- const [used] = await db<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
84
+ });
85
+ return { cost, group, rows };
86
+ },
87
+ async settle(reservation: GeoReservation, referenceCost: number) {
88
+ const refund = reservation.cost - referenceCost;
89
+ if (!Number.isSafeInteger(referenceCost) || referenceCost < 0 || !Number.isSafeInteger(refund))
90
+ throw new Error("Invalid geo settlement");
91
+ if (!refund) return;
92
+ // Negative refunds truthfully debit known routing overruns. Admission still uses the original
93
+ // bounded CAS transaction; future reservations see the overdrawn counters and are refused.
94
+ // Settle once per reservation, in the same fixed row order as admission.
95
+ await db().begin(async (tx) => {
96
+ for (const row of reservation.rows) {
97
+ await tx`UPDATE geo.usage SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period}`;
98
+ await tx`UPDATE geo.shares SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period} AND function_group=${reservation.group}`;
99
+ }
100
+ });
101
+ },
102
+ async availability(subject: GeoBudgetSubject, config: GeoBudgetConfig, now: Date) {
103
+ const balances = [];
104
+ for (const row of rowsFor(subject, config, now)) {
105
+ const [used] = await db()<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
98
106
  WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period}`;
99
- balances.push({ exhausted: (used?.used ?? 0) >= row.limit, resetsAt: row.resetsAt });
100
- }
101
- return { cloud: balances[0]!, person: balances[1]! };
102
- },
103
- async recordTransition(
104
- event: "provider_key_rejected" | "monthly_budget_exhausted" | "route_cost_overrun",
105
- timezone: string,
106
- context: { actor: CapabilityExecutionContext["actor"] | AppPwaActor; requestId?: string },
107
- now: Date,
108
- metadata: Record<string, number> = {},
109
- ) {
110
- const period = geoPeriods(now, timezone).cloud.period;
111
- await db.begin(async (tx) => {
112
- const inserted =
113
- event === "route_cost_overrun"
114
- ? [true]
115
- : await tx`INSERT INTO geo.audit_transitions(event,period) VALUES(${event},${period}) ON CONFLICT DO NOTHING RETURNING event`;
116
- if (!inserted.length) return;
117
- const actor =
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 };
128
- await audit.record(
129
- {
130
- action: `core.geo.${event}`,
131
- outcome: event === "monthly_budget_exhausted" ? "denied" : "failed",
132
- actor,
133
- requestId: context.requestId,
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,
107
+ balances.push({ exhausted: (used?.used ?? 0) >= row.limit, resetsAt: row.resetsAt });
108
+ }
109
+ return { cloud: balances[0]!, person: balances[1]! };
110
+ },
111
+ async recordTransition(
112
+ event: "provider_key_rejected" | "monthly_budget_exhausted" | "route_cost_overrun",
113
+ timezone: string,
114
+ context: { actor: CapabilityExecutionContext["actor"] | CapabilityAppExecutionContext["actor"] | AppPwaActor; requestId?: string },
115
+ now: Date,
116
+ metadata: Record<string, number> = {},
117
+ ) {
118
+ const period = geoPeriods(now, timezone).cloud.period;
119
+ await db().begin(async (tx) => {
120
+ const inserted =
121
+ event === "route_cost_overrun"
122
+ ? [true]
123
+ : await tx`INSERT INTO geo.audit_transitions(event,period) VALUES(${event},${period}) ON CONFLICT DO NOTHING RETURNING event`;
124
+ if (!inserted.length) return;
125
+ const actor =
126
+ context.actor.kind === "device" || context.actor.kind === "visit"
127
+ ? null
128
+ : context.actor.kind === "app"
129
+ ? { uid: `app:${context.actor.appId}`, provider: "app", roles: [] }
130
+ : context.actor.kind === "user"
131
+ ? {
132
+ userId: context.actor.user.id,
133
+ uid: context.actor.user.uid,
134
+ provider: context.actor.user.provider,
135
+ roles: context.actor.user.roles,
136
+ }
137
+ : { uid: `service-account:${context.actor.serviceAccount.id}`, provider: "service_account", roles: context.actor.scopes };
138
+ await audit.record(
139
+ {
140
+ action: `core.geo.${event}`,
141
+ outcome: event === "monthly_budget_exhausted" ? "denied" : "failed",
142
+ actor,
143
+ requestId: context.requestId,
144
+ metadata: {
145
+ period,
146
+ ...metadata,
147
+ ...(context.actor.kind === "app" ? { appId: context.actor.appId, keyId: context.actor.keyId } : {}),
148
+ ...(context.actor.kind === "device" || context.actor.kind === "visit"
149
+ ? {
143
150
  appId: context.actor.appId,
144
- deviceId: context.actor.deviceId,
145
- ...(context.actor.kind === "visit" ? { visitId: context.actor.visitId } : {}),
146
- },
147
- }
148
- : {}),
151
+ subject: { type: "user", userId: context.actor.ownerUserId },
152
+ via: {
153
+ kind: context.actor.kind,
154
+ appId: context.actor.appId,
155
+ deviceId: context.actor.deviceId,
156
+ ...(context.actor.kind === "visit" ? { visitId: context.actor.visitId } : {}),
157
+ },
158
+ }
159
+ : {}),
160
+ },
149
161
  },
150
- },
151
- tx,
152
- );
153
- });
154
- },
155
- });
162
+ tx,
163
+ );
164
+ });
165
+ },
166
+ };
167
+ };
@@ -1,4 +1,5 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
+ import { sql } from "../../runtime/clients";
2
3
  export const migrate = async (db: SQL = sql): Promise<void> => {
3
4
  await db.begin(async (tx) => {
4
5
  await tx`SELECT pg_advisory_xact_lock(hashtextextended('core.geo.migrations', 0))`;
@@ -6,9 +6,9 @@ import type { createGeoCache } from "./cache";
6
6
  import { geoCacheKey } from "./cache";
7
7
  import { geoReferenceCost } from "./costs";
8
8
  import { GeoError, GeoProviderError, limitExceeded } from "./errors";
9
+ import { type GeoContext, geoIdentity } from "./identity";
9
10
  import type { createGeoLedger } from "./ledger";
10
11
  import { geoRouteReferenceCost } from "./route-costs";
11
- import type { GeoContext } from "./service";
12
12
  import { type GeoConfig, geoPlan } from "./settings";
13
13
 
14
14
  export type GeoRouteChainDependencies = {
@@ -42,14 +42,14 @@ export const runGeoRouteChain = async <T>(
42
42
  work: (call: GeoRouteChainCall) => Promise<T>,
43
43
  ): Promise<T> => {
44
44
  const { ledger, bucket, cache, now } = dependencies;
45
- if (!context.accessSubject) throw new GeoError("GEO_FORBIDDEN");
45
+ const { accessSubject } = geoIdentity(context);
46
46
  context.signal?.throwIfAborted();
47
47
  const maximumCredits = Math.min(config.personDailyCredits, config.monthlyCredits);
48
48
  if (reserveCost > maximumCredits * 1000) limitExceeded("reservationCredits", maximumCredits, reserveCost / 1000);
49
49
  let reservation: Awaited<ReturnType<typeof ledger.reserve>>;
50
50
  try {
51
51
  // One atomic admission for all known geocodes and every possible paid routing step.
52
- reservation = await ledger.reserve(context.accessSubject, config, reserveCost, "route", now());
52
+ reservation = await ledger.reserve(accessSubject, config, reserveCost, "route", now());
53
53
  } catch (error) {
54
54
  if (error instanceof GeoError && error.code === "GEO_BUDGET_EXHAUSTED" && error.details.scope === "cloud")
55
55
  await dependencies.recordTransition("monthly_budget_exhausted", config.timezone, context);
@@ -1,6 +1,4 @@
1
1
  import type { z } from "zod";
2
- import type { AppPwaActor } from "../../app-pwa/contracts";
3
- import type { AccessSubject, CapabilityExecutionContext, RequestActor } from "../../contracts";
4
2
  import {
5
3
  GeoAutocompleteDataSchema,
6
4
  type GeoAutocompleteInput,
@@ -26,28 +24,10 @@ import { cutGeoWindow, decodePolyline6, locateOnGeoLine, simplifyGeoLine } from
26
24
  import { createGeoRouteService } from "./route-service";
27
25
  import { type GeoConfig, geoPlan, readGeoConfig } from "./settings";
28
26
 
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;
33
- export const geoIdentity = (context: GeoContext): { actor: RequestActor; accessSubject: AccessSubject } => {
34
- const { actor, accessSubject } = context;
35
- if (
36
- !actor ||
37
- !accessSubject ||
38
- !(actor.kind === "user" ? actor.user?.id : actor.kind === "service_account" ? actor.serviceAccount?.id : undefined) ||
39
- !(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
40
- )
41
- throw new GeoError("GEO_FORBIDDEN");
42
- return { actor, accessSubject };
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
- };
27
+ export { type AppPwaGeoContext, type GeoContext, geoIdentity } from "./identity";
28
+
29
+ import { type AppPwaGeoContext, type GeoCallContext, type GeoContext, geoCallIdentity, geoIdentity } from "./identity";
30
+
51
31
  const bound = (name: string, value: number, min: number, max: number, integer = false) => {
52
32
  if (!Number.isFinite(value) || value > max || value < min || (integer && !Number.isInteger(value)))
53
33
  limitExceeded(name, value < min ? min : max, value);
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import { HTTPException } from "hono/http-exception";
3
2
  import {
4
3
  DBIP_ATTRIBUTION,
@@ -8,6 +7,7 @@ import {
8
7
  type GeoIpAdminStatus,
9
8
  type GeoIpKind,
10
9
  } from "../../contracts/geoip";
10
+ import { sql } from "../../runtime/clients";
11
11
  import { createGeoIp, type GeoIp, type GeoIpDatabaseInfo, getGeoIp } from "../../server/request-context/geoip";
12
12
  import { audit } from "../audit";
13
13
  import { GeoIpError, parseGeoIpUpload, readGeoIpUpload } from "./upload";
@@ -79,11 +79,11 @@ const serialized = <T>(write: () => Promise<T>): Promise<T> => {
79
79
  return run;
80
80
  };
81
81
  export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; bundled?: () => ReturnType<GeoIp["status"]> } = {}) => {
82
- const db = options.db ?? sql;
82
+ const db = () => options.db ?? sql;
83
83
  const reader = () => options.reader ?? getGeoIp();
84
84
  const status = async (actor: Actor, config: { now?: () => number } = {}): Promise<GeoIpAdminStatus> => {
85
85
  requireAdmin(actor);
86
- const rows = await db<GeoIpUploadInfo[]>`
86
+ const rows = await db()<GeoIpUploadInfo[]>`
87
87
  SELECT d.kind, d.sha256, (d.metadata->>'size')::int AS size, d.database_type, d.build_epoch,
88
88
  d.metadata->'description'->>'en' AS description, d.uploaded_at, d.uploaded_by,
89
89
  COALESCE(NULLIF(u.display_name, ''), u.uid) AS uploader_name
@@ -103,7 +103,7 @@ export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; b
103
103
  database.info.sha256 = sha256;
104
104
  config.signal?.throwIfAborted();
105
105
  await serialized(async () => {
106
- await db.begin(async (tx) => {
106
+ await db().begin(async (tx) => {
107
107
  await tx`
108
108
  INSERT INTO geoip.databases (kind, data, sha256, database_type, build_epoch, metadata, uploaded_by)
109
109
  VALUES (${kind}, ${bytes}, ${sha256}, ${metadata.databaseType}, ${metadata.buildEpoch}::timestamptz,
@@ -132,7 +132,7 @@ export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; b
132
132
  async reset(actor: Actor, kind: GeoIpKind) {
133
133
  requireAdmin(actor);
134
134
  const changed = await serialized(async () => {
135
- const deleted = await db.begin(async (tx) => {
135
+ const deleted = await db().begin(async (tx) => {
136
136
  const [row] = await tx<{ sha256: string; database_type: string; build_epoch: Date; size: number }[]>`
137
137
  DELETE FROM geoip.databases WHERE kind = ${kind}
138
138
  RETURNING sha256, database_type, build_epoch, octet_length(data) AS size`;
@@ -1,30 +1,33 @@
1
- import { sql } from "bun";
2
1
  import { GEOIP_MAX_DATABASE_BYTES, type GeoIpKind } from "../../contracts/geoip";
2
+ import { sql } from "../../runtime/clients";
3
3
  import type { GeoIpUploads } from "../../server/request-context/geoip";
4
4
 
5
5
  const SLICE_BYTES = 8 * 1024 * 1024;
6
- export const createGeoIpUploads = (db: typeof sql = sql): GeoIpUploads => ({
7
- async versions() {
8
- const rows = await db<{ kind: GeoIpKind; sha256: string }[]>`SELECT kind, sha256 FROM geoip.databases`;
9
- return Object.fromEntries(rows.map((row) => [row.kind, row.sha256]));
10
- },
11
- async read(kind, sha256) {
12
- const [row] = await db<{ size: number }[]>`
6
+ export const createGeoIpUploads = (injectedDb?: typeof sql): GeoIpUploads => {
7
+ const db = () => injectedDb ?? sql;
8
+ return {
9
+ async versions() {
10
+ const rows = await db()<{ kind: GeoIpKind; sha256: string }[]>`SELECT kind, sha256 FROM geoip.databases`;
11
+ return Object.fromEntries(rows.map((row) => [row.kind, row.sha256]));
12
+ },
13
+ async read(kind, sha256) {
14
+ const [row] = await db()<{ size: number }[]>`
13
15
  SELECT octet_length(data) AS size FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
14
- if (!row) return null;
15
- if (row.size < 1 || row.size > GEOIP_MAX_DATABASE_BYTES) throw new Error("Invalid GeoIP database size");
16
- const bytes = Buffer.allocUnsafe(row.size);
17
- for (let offset = 0; offset < bytes.length; offset += SLICE_BYTES) {
18
- const length = Math.min(SLICE_BYTES, bytes.length - offset);
19
- const [slice] = await db<{ data: Uint8Array }[]>`
16
+ if (!row) return null;
17
+ if (row.size < 1 || row.size > GEOIP_MAX_DATABASE_BYTES) throw new Error("Invalid GeoIP database size");
18
+ const bytes = Buffer.allocUnsafe(row.size);
19
+ for (let offset = 0; offset < bytes.length; offset += SLICE_BYTES) {
20
+ const length = Math.min(SLICE_BYTES, bytes.length - offset);
21
+ const [slice] = await db()<{ data: Uint8Array }[]>`
20
22
  SELECT substring(data FROM ${offset + 1} FOR ${length}) AS data
21
23
  FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
22
- if (!slice) return null;
23
- if (slice.data.byteLength !== length) throw new Error("Incomplete GeoIP database slice");
24
- bytes.set(slice.data, offset);
25
- }
26
- return bytes;
27
- },
28
- });
24
+ if (!slice) return null;
25
+ if (slice.data.byteLength !== length) throw new Error("Incomplete GeoIP database slice");
26
+ bytes.set(slice.data, offset);
27
+ }
28
+ return bytes;
29
+ },
30
+ };
31
+ };
29
32
 
30
33
  export const geoIpUploads = createGeoIpUploads();
@@ -1,8 +1,9 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { appAudienceRoles, hasAnyAppRole } from "../../_internal/app-roles";
3
3
  import { getApp, listApps } from "../../_internal/registry";
4
4
  import type { AppRegistryEntry } from "../../contracts/registry";
5
5
  import type { RequestActor } from "../../contracts/shared";
6
+ import { sql } from "../../runtime/clients";
6
7
  import type { RequestAuthority } from "../../server/middleware/auth";
7
8
  import { resolveAppPresentations } from "../../shared/app-presentation";
8
9
  import { type HelpArticleLink, helpLocaleChain, parseHelpReference } from "../../shared/help";
@@ -1,5 +1,6 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { APP_REGISTRY_TTL_MS } from "../../_internal/registry";
3
+ import { sql } from "../../runtime/clients";
3
4
  import type { HelpAccessLevel, HelpKind } from "../../shared/help";
4
5
  import { ensureSchema, toPgTextArray } from "../postgres";
5
6
  import type { HelpCorpus, HelpMetadata } from "./types";
@@ -0,0 +1,44 @@
1
+ import { getAppWorkloadCredential } from "../../_internal/workload-credential";
2
+ import { parseAppKey } from "../../app-key";
3
+ import { env } from "../../config/env";
4
+ import { CapabilityAppIdSchema } from "../../contracts/capabilities";
5
+ import { signAppProof } from "../../runtime/bootstrap-protocol";
6
+
7
+ /** Send only to Core. Call for every transmission, including safe caller-owned retries. */
8
+ export const sendAppRequest = async (
9
+ request: Request,
10
+ options: {
11
+ appId: string;
12
+ transport?: (request: Request) => Promise<Response>;
13
+ /** Release N legacy authority; evaluated only when no app key is configured. */
14
+ bearer?: () => string | undefined;
15
+ },
16
+ ): Promise<Response> => {
17
+ const appId = CapabilityAppIdSchema.parse(options.appId);
18
+ const headers = new Headers(request.headers);
19
+ const declaredApp = headers.get("X-Cloud-App-Id");
20
+ if (declaredApp !== null && declaredApp !== appId) throw new Error("App identity header does not match the calling application");
21
+ headers.set("X-Cloud-App-Id", appId);
22
+ const keyValue = env.CLOUD_APP_KEY;
23
+ if (keyValue) {
24
+ const key = parseAppKey(keyValue);
25
+ if (key.appId !== options.appId) throw new Error("App-key appId does not match the calling application");
26
+ const url = new URL(request.url);
27
+ const proof = await signAppProof(key, {
28
+ method: request.method,
29
+ path: url.pathname + url.search,
30
+ body: new Uint8Array(await request.clone().arrayBuffer()),
31
+ });
32
+ headers.set("authorization", `Bearer ${proof}`);
33
+ } else if (!headers.has("authorization")) {
34
+ const token = (options.bearer ?? getAppWorkloadCredential)();
35
+ if (!token) throw new Error("App workload authorization is unavailable");
36
+ headers.set("authorization", `Bearer ${token}`);
37
+ }
38
+ // A proof is bound to this target; redirects must never carry it elsewhere.
39
+ return (options.transport ?? fetch)(new Request(request, { headers, redirect: "error" }));
40
+ };
41
+
42
+ /** Legacy workload tokens and the OAuth secret never contain JWT separators. */
43
+ export const isAppRequestProof = (authorization: string | undefined): boolean =>
44
+ authorization?.startsWith("Bearer ") === true && authorization.slice(7).includes(".");
@@ -1,6 +1,7 @@
1
1
  export const CLOUD_SESSION_JWKS_PATH = "/.well-known/cloud-session-jwks.json";
2
2
  export const CLOUD_INVOCATION_JWKS_PATH = "/.well-known/cloud-invocation-jwks.json";
3
3
  export const CLOUD_OAUTH_JWKS_PATH = "/.well-known/jwks.json";
4
+ export const CLOUD_INTERNAL_OAUTH_JWKS_PATH = "/api/_internal/identity/v1/oauth/jwks";
4
5
  export const CLOUD_SESSION_TOKEN_TYPE = "cloud-session+jwt";
5
6
  export const CLOUD_SESSION_AUDIENCE = "cloud";
6
7
  export const CLOUD_INVOCATION_TOKEN_TYPE = "cloud-invocation+jwt";
@@ -1,4 +1,5 @@
1
1
  export { getAppWorkloadCredential } from "../../_internal/workload-credential";
2
+ export { isAppRequestProof, sendAppRequest } from "./app-request";
2
3
  export {
3
4
  CLOUD_INVOCATION_JWKS_PATH,
4
5
  CLOUD_INVOCATION_PROTOCOL_VERSION,
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
+
2
3
  import type { RequestActor } from "../../server/middleware/auth";
3
4
  import type { AccessSubject } from "../../server/services/access";
4
5
  import { isAccountCategoryAllowed } from "../account-category-policy";