@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,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { err, fail, ok, type Result } from "@k2b/stdlib";
3
3
  import type { Worker } from "@k2b/sync";
4
- import { sql } from "bun";
5
4
  import { lazySync } from "../../_internal/process-sync";
6
5
  import { type MailMessage, MailMessageSchema, type MailStatus, MailStatusSchema } from "../../contracts/outgoing-mail";
6
+ import { sql } from "../../runtime/clients";
7
7
  import { markdown } from "../../shared/markdown";
8
8
  import { type AuditActor, audit } from "../audit";
9
9
  import { logger, trace } from "../logging";
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sql } from "bun";
3
2
  import webpush from "web-push";
4
3
  import { z } from "zod";
5
4
  import { notificationGroupTag } from "../../contracts/notification-group";
6
5
  import { isSafeNotificationTargetHref } from "../../contracts/notification-types";
7
6
  import { type BrowserPushSubscription, BrowserPushSubscriptionSchema } from "../../contracts/user-notifications";
7
+ import { sql } from "../../runtime/clients";
8
8
  import { decryptSecret, encryptSecret } from "../secrets";
9
9
  import { coreSettings } from "../settings/api";
10
10
  import { SettingUnreadableError } from "../settings/store";
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { AnyBoundNotificationDefinition } from "../../contracts/notification-types";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { logger } from "../logging";
4
4
  import { toPgTextArray } from "../postgres";
5
5
 
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import { z } from "zod";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { decryptSecret } from "../secrets";
4
4
  import { getNotificationChannel } from "./channels";
5
5
  import { quietStateForEvent } from "./quiet";
@@ -1,7 +1,7 @@
1
- import { sql } from "bun";
2
1
  import type { ZodType } from "zod";
3
2
  import type { BoundNotificationDefinition, NotificationRecipientKind, NotificationSendInput } from "../../contracts/notification-types";
4
3
  import type { PaginationParams } from "../../contracts/shared";
4
+ import { sql } from "../../runtime/clients";
5
5
  import { logger } from "../logging";
6
6
  import { escapeLikePattern } from "../postgres";
7
7
  import { notificationMailOutcome, sendNotificationMail } from "./email-mail";
@@ -1,6 +1,7 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { MailStatus } from "../../contracts/outgoing-mail";
3
3
  import type { NotificationDeliveryStatus } from "../../contracts/user-notifications";
4
+ import { sql } from "../../runtime/clients";
4
5
  import { escapeLikePattern, toPgTextArray } from "../postgres";
5
6
 
6
7
  const DELIVERY_STATUSES = new Set<NotificationDeliveryStatus>(["deferred", "pending", "sending", "delivered", "suppressed", "failed"]);
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import type { output, ZodType } from "zod";
3
2
  import { isNotificationGroup, notificationGroupTag } from "../../contracts/notification-group";
4
3
  import type {
@@ -10,6 +9,7 @@ import type {
10
9
  NotificationSendInput,
11
10
  } from "../../contracts/notification-types";
12
11
  import { validateNotificationTargetHref } from "../../contracts/notification-types";
12
+ import { sql } from "../../runtime/clients";
13
13
  import { normalizeLocale } from "../../shared/locale";
14
14
  import { logger } from "../logging";
15
15
  import { encryptSecret } from "../secrets";
@@ -1,5 +1,4 @@
1
1
  import { dates } from "@k2b/stdlib";
2
- import { sql } from "bun";
3
2
  import type { MutationResult } from "../../contracts/shared";
4
3
  import {
5
4
  NOTIFICATION_DO_NOT_DISTURB_MAX_DAYS,
@@ -9,6 +8,7 @@ import {
9
8
  type NotificationQuietState,
10
9
  type UpdateNotificationQuietSettings,
11
10
  } from "../../contracts/user-notifications";
11
+ import { sql } from "../../runtime/clients";
12
12
  import { normalizeTimeZone } from "../../shared/time";
13
13
 
14
14
  const DAY_MS = 24 * 60 * 60_000;
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import {
3
2
  isSafeNotificationTargetHref,
4
3
  type NotificationDefinitionPresentationCatalog,
@@ -11,6 +10,7 @@ import type {
11
10
  UserNotificationPreference,
12
11
  UserNotificationPreferencesResponse,
13
12
  } from "../../contracts/user-notifications";
13
+ import { sql } from "../../runtime/clients";
14
14
  import { normalizeLocale } from "../../shared/locale";
15
15
  import { toPgTextArray } from "../postgres";
16
16
  import { listNotificationChannels } from "./channels";
@@ -1,6 +1,6 @@
1
- import { sql } from "bun";
2
1
  import { createRemoteJWKSet, decodeProtectedHeader, errors, type JWTPayload, type JWTVerifyGetKey, jwtVerify } from "jose";
3
2
  import type { User } from "../contracts/shared";
3
+ import { sql } from "../runtime/clients";
4
4
  import { isAccountCategoryAllowed } from "./account-category-policy";
5
5
  import { isAccountExpired } from "./account-model";
6
6
  import { IDENTITY_JWKS_MAX_AGE_SECONDS } from "./identity/constants";
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../runtime/clients";
2
+
2
3
  import { logger } from "./logging";
3
4
 
4
5
  /**
@@ -66,9 +67,9 @@ export type PgOutbox<Row extends OutboxRow> = {
66
67
  stop(): Promise<void>;
67
68
  };
68
69
 
69
- const identifier = (name: string, label: string): ReturnType<typeof sql.unsafe> => {
70
+ const identifier = (name: string, label: string) => {
70
71
  if (!name.split(".").every((part) => IDENTIFIER.test(part))) throw new Error(`Invalid outbox ${label}: ${name}`);
71
- return sql.unsafe(name);
72
+ return name;
72
73
  };
73
74
 
74
75
  export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row>): PgOutbox<Row> => {
@@ -81,7 +82,7 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
81
82
  const log = logger(config.name);
82
83
  const filters = Object.entries(config.where ?? {}).map(([column, value]) => ({ column: identifier(column, "filter column"), value }));
83
84
  const matches = (alias: "current" | "busy") =>
84
- filters.reduce((fragment, { column, value }) => sql`${fragment} AND ${sql.unsafe(alias)}.${column} = ${value}`, sql``);
85
+ filters.reduce((fragment, { column, value }) => sql`${fragment} AND ${sql.unsafe(alias)}.${sql.unsafe(column)} = ${value}`, sql``);
85
86
  // Sorted, so the order of the `where` entries cannot give one outbox two locks. Descending keeps the
86
87
  // lock that live outboxes (`kind`, `app_id`) always took, so replicas of two releases share it.
87
88
  const where = Object.entries(config.where ?? {}).sort(([left], [right]) => (left < right ? 1 : -1));
@@ -115,28 +116,28 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
115
116
  return tx<Row[]>`
116
117
  WITH candidates AS MATERIALIZED (
117
118
  SELECT current.id
118
- FROM ${table} current
119
+ FROM ${sql.unsafe(table)} current
119
120
  WHERE (current.attempts = 0 OR current.next_attempt_at <= now())
120
121
  AND (current.claimed_until IS NULL OR current.claimed_until <= now())
121
122
  ${matches("current")}
122
123
  AND NOT EXISTS (
123
124
  SELECT
124
- FROM ${table} busy
125
- WHERE busy.${runColumn} = current.${runColumn}
125
+ FROM ${sql.unsafe(table)} busy
126
+ WHERE busy.${sql.unsafe(runColumn)} = current.${sql.unsafe(runColumn)}
126
127
  AND (busy.claimed_until > now() OR (busy.attempts > 0 AND busy.next_attempt_at > now()))
127
128
  ${matches("busy")}
128
129
  OFFSET 0
129
130
  )
130
- ORDER BY current.${sequence}
131
+ ORDER BY current.${sql.unsafe(sequence)}
131
132
  LIMIT ${cap}
132
133
  FOR UPDATE SKIP LOCKED
133
134
  ), claimed AS (
134
- UPDATE ${table} outbox
135
+ UPDATE ${sql.unsafe(table)} outbox
135
136
  SET claimed_until = now() + (${claimMs} * interval '1 millisecond')
136
137
  WHERE outbox.id = ANY(ARRAY(SELECT id FROM candidates))
137
138
  RETURNING outbox.*
138
139
  )
139
- SELECT * FROM claimed ORDER BY ${sequence}
140
+ SELECT * FROM claimed ORDER BY ${sql.unsafe(sequence)}
140
141
  `;
141
142
  });
142
143
 
@@ -145,7 +146,7 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
145
146
  const message = error instanceof Error ? error.message : String(error);
146
147
  const delaySeconds = Math.min(300, 2 ** Math.min(attempts, 8));
147
148
  await sql`
148
- UPDATE ${table}
149
+ UPDATE ${sql.unsafe(table)}
149
150
  SET attempts = ${attempts},
150
151
  next_attempt_at = now() + (${delaySeconds} * interval '1 second'),
151
152
  claimed_until = NULL,
@@ -158,7 +159,7 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
158
159
  const dispatch: PgOutbox<Row>["dispatch"] = async (row, publish = config.publish) => {
159
160
  try {
160
161
  await publish(row);
161
- await sql`DELETE FROM ${table} WHERE id = ${row.id}::uuid AND attempts = ${row.attempts}`;
162
+ await sql`DELETE FROM ${sql.unsafe(table)} WHERE id = ${row.id}::uuid AND attempts = ${row.attempts}`;
162
163
  } catch (error) {
163
164
  await retryLater(row, error);
164
165
  }
@@ -182,7 +183,7 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
182
183
  const rest = run.slice(index + 1).map((later) => later.id);
183
184
  // Past the claim, the rest is free already, and releasing it could end another replica's claim.
184
185
  if (rest.length > 0 && performance.now() < claimEnds) {
185
- await sql`UPDATE ${table} SET claimed_until = NULL WHERE id = ANY(${sql.array(rest, "uuid")})`;
186
+ await sql`UPDATE ${sql.unsafe(table)} SET claimed_until = NULL WHERE id = ANY(${sql.array(rest, "uuid")})`;
186
187
  }
187
188
  return;
188
189
  }
@@ -190,7 +191,7 @@ export const createPgOutbox = <Row extends OutboxRow>(config: PgOutboxConfig<Row
190
191
  }
191
192
  }),
192
193
  );
193
- if (delivered.length > 0) await sql`DELETE FROM ${table} WHERE id = ANY(${sql.array(delivered, "uuid")})`;
194
+ if (delivered.length > 0) await sql`DELETE FROM ${sql.unsafe(table)} WHERE id = ANY(${sql.array(delivered, "uuid")})`;
194
195
  for (const result of results) if (result.status === "rejected") throw result.reason;
195
196
  };
196
197
 
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import {
3
2
  type AdminMailFilter,
4
3
  type AdminMailRecord,
@@ -6,6 +5,7 @@ import {
6
5
  type MailPage,
7
6
  type MailPageParams,
8
7
  } from "../../contracts/outgoing-mail";
8
+ import { sql } from "../../runtime/clients";
9
9
  import { audit } from "../audit";
10
10
  import { cleanupMailAttachments, publishMailSettled } from "./dispatcher";
11
11
  import { adminMessageRecord, type MessageRow, outgoingMailMessages } from "./messages";
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Socket } from "node:net";
3
3
  import { Readable } from "node:stream";
4
- import { sql } from "bun";
5
4
  import { z } from "zod";
6
5
  import { listApps } from "../../_internal/registry";
6
+ import { sql } from "../../runtime/clients";
7
7
  import { DownstreamBusyError, downstreamBudget } from "../downstream-budget";
8
8
  import { parsePgJsonValue } from "../postgres";
9
9
  import { get } from "../settings";
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
+
2
3
  import { mailNextDrainDelay } from "./bulk";
3
4
  import { attemptOutgoingMail } from "./dispatcher";
4
5
  import type { MessageRow } from "./messages";
@@ -1,5 +1,6 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { MailMessage } from "../../contracts/outgoing-mail";
3
+ import { sql } from "../../runtime/clients";
3
4
  import {
4
5
  cancelMailStreams,
5
6
  deleteMailObjects,
@@ -1,5 +1,5 @@
1
1
  import type { ObjectRef } from "@k2b/sync";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import { z } from "zod";
4
4
  import {
5
5
  type AdminMailFilter,
@@ -14,6 +14,7 @@ import {
14
14
  MailRecordSchema,
15
15
  type MailStatus,
16
16
  } from "../../contracts/outgoing-mail";
17
+ import { sql } from "../../runtime/clients";
17
18
  import { sanitizeEmailHtml } from "../../shared/email-html";
18
19
  import { audit } from "../audit";
19
20
  import { escapeLikePattern, parsePgJsonValue, toPgTextArray, toPgUuidArray } from "../postgres";
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { MailRetention } from "../../contracts/outgoing-mail";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { toPgUuidArray } from "../postgres";
4
4
  import { get, read } from "../settings";
5
5
  import { deleteMailObjects } from "./attachments";
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { MailMessage, MailRecord } from "../../contracts/outgoing-mail";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { cancelMailStreams, deleteMailObjects, uploadMailAttachments } from "./attachments";
4
4
  import {
5
5
  allowedMailProfile,
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import { listApps } from "../../_internal/registry";
4
4
  import {
5
5
  type AdminMailApp,
@@ -18,6 +18,7 @@ import {
18
18
  MailProfileKeySchema,
19
19
  } from "../../contracts/outgoing-mail";
20
20
  import type { AppRegistryEntry } from "../../contracts/registry";
21
+ import { sql } from "../../runtime/clients";
21
22
  import { type AuditActor, audit } from "../audit";
22
23
  import { toPgTextArray } from "../postgres";
23
24
  import { decryptValue, encryptValue } from "../settings/crypto";
@@ -1,5 +1,6 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
3
+ import { sql } from "../runtime/clients";
3
4
 
4
5
  type ConsentAnswer = { allowed: boolean; revision: number };
5
6
  export class PanelConsentCancelled extends Error {
@@ -12,34 +13,36 @@ export class PanelConsentCancelled extends Error {
12
13
  * Core owns the table; platform requests read without a process-wide cache. Missing is distinct from denied. Reads
13
14
  * take a share lock, so a page rendered while a save is in flight sees that save's outcome, not the answer before it.
14
15
  */
15
- export const createPanelContextConsentService = (db: SQL = sql) => ({
16
- async get(userId: string, provider: string): Promise<ConsentAnswer | null> {
17
- CapabilityAppIdSchema.parse(provider);
18
- const [row] = await db<ConsentAnswer[]>`
16
+ export const createPanelContextConsentService = (injectedDb?: SQL) => {
17
+ const db = () => injectedDb ?? sql;
18
+ return {
19
+ async get(userId: string, provider: string): Promise<ConsentAnswer | null> {
20
+ CapabilityAppIdSchema.parse(provider);
21
+ const [row] = await db()<ConsentAnswer[]>`
19
22
  SELECT allowed, revision FROM auth.panel_context_consent WHERE user_id = ${userId}::uuid AND provider = ${provider}
20
23
  FOR SHARE
21
24
  `;
22
- return row ?? null;
23
- },
24
- async list(userId: string, providers: string[]): Promise<Record<string, ConsentAnswer>> {
25
- if (!providers.length) return {};
26
- for (const provider of providers) CapabilityAppIdSchema.parse(provider);
27
- const rows = await db<({ provider: string } & ConsentAnswer)[]>`
25
+ return row ?? null;
26
+ },
27
+ async list(userId: string, providers: string[]): Promise<Record<string, ConsentAnswer>> {
28
+ if (!providers.length) return {};
29
+ for (const provider of providers) CapabilityAppIdSchema.parse(provider);
30
+ const rows = await db()<({ provider: string } & ConsentAnswer)[]>`
28
31
  SELECT provider, allowed, revision FROM auth.panel_context_consent
29
- WHERE user_id = ${userId}::uuid AND provider IN ${db(providers)}
32
+ WHERE user_id = ${userId}::uuid AND provider IN ${db()(providers)}
30
33
  FOR SHARE
31
34
  `;
32
- return Object.fromEntries(rows.map(({ provider, allowed, revision }) => [provider, { allowed, revision }]));
33
- },
34
- /**
35
- * Compare-and-set on the revision the client read. A save whose request was cancelled (the browser gave up or
36
- * left) is rolled back before commit, so no answer lands after its page stopped waiting.
37
- */
38
- async set(userId: string, provider: string, allowed: boolean, revision: number, signal?: AbortSignal): Promise<ConsentAnswer | null> {
39
- CapabilityAppIdSchema.parse(provider);
40
- return db.begin(async (tx) => {
41
- await tx`SET LOCAL statement_timeout = '5s'`;
42
- const [row] = await tx<ConsentAnswer[]>`
35
+ return Object.fromEntries(rows.map(({ provider, allowed, revision }) => [provider, { allowed, revision }]));
36
+ },
37
+ /**
38
+ * Compare-and-set on the revision the client read. A save whose request was cancelled (the browser gave up or
39
+ * left) is rolled back before commit, so no answer lands after its page stopped waiting.
40
+ */
41
+ async set(userId: string, provider: string, allowed: boolean, revision: number, signal?: AbortSignal): Promise<ConsentAnswer | null> {
42
+ CapabilityAppIdSchema.parse(provider);
43
+ return db().begin(async (tx) => {
44
+ await tx`SET LOCAL statement_timeout = '5s'`;
45
+ const [row] = await tx<ConsentAnswer[]>`
43
46
  INSERT INTO auth.panel_context_consent (user_id, provider, allowed, revision)
44
47
  SELECT ${userId}::uuid, ${provider}, ${allowed}, 1
45
48
  WHERE ${revision} = 0 OR EXISTS (
@@ -50,9 +53,10 @@ export const createPanelContextConsentService = (db: SQL = sql) => ({
50
53
  WHERE auth.panel_context_consent.revision = ${revision}
51
54
  RETURNING allowed, revision
52
55
  `;
53
- if (signal?.aborted) throw new PanelConsentCancelled();
54
- return row ?? null;
55
- });
56
- },
57
- });
56
+ if (signal?.aborted) throw new PanelConsentCancelled();
57
+ return row ?? null;
58
+ });
59
+ },
60
+ };
61
+ };
58
62
  export const panelContextConsent = createPanelContextConsentService();
@@ -1,4 +1,4 @@
1
- import { redis } from "bun";
1
+ import { redis } from "../../../runtime/clients";
2
2
 
3
3
  /** Requested by email or username on the login page; resolves the account through its email at use. */
4
4
  export type EmailLoginTokenPayload = { email: string; category?: "guest" | "login" };
@@ -1,6 +1,6 @@
1
- import { sql } from "bun";
2
1
  import type { MutationResult, UserProfile } from "../../../contracts/shared";
3
2
  import { coreUserEventPublisher } from "../../../events/core-user-created";
3
+ import { sql } from "../../../runtime/clients";
4
4
  import { isAccountCategoryAllowed } from "../../account-category-policy";
5
5
  import { writeDeletedAccountAudit } from "../../account-lifecycle/audit";
6
6
  import { emailAlreadyUsed, findAccountsByEmail, lockAccountEmail, normalizeAccountEmail } from "../../accounts/email-write";
@@ -1,8 +1,9 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import type { Context } from "hono";
4
4
  import { lazySync } from "../_internal/process-sync";
5
5
  import { PWA_LIMITS as limits, type PwaDeviceView, type PwaErrorCode, type PwaPairingState, type PwaPlatform } from "../contracts/pwa";
6
+ import { sql } from "../runtime/clients";
6
7
  import { isAccountCategoryAllowed } from "./account-category-policy";
7
8
  import { audit } from "./audit";
8
9
  import { logger } from "./logging";
@@ -1,17 +1,20 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { defaultRailPreferences, type RailPreferences, RailPreferencesSchema } from "../contracts/rail-preferences";
3
+ import { sql } from "../runtime/clients";
3
4
 
4
5
  /** Core-owned data, read by the platform SSR seam without a process-wide cache. */
5
- export const createRailPreferencesService = (db: SQL = sql) => ({
6
- async get(userId: string): Promise<RailPreferences> {
7
- const [row] = await db<{ revision: number; visibility: unknown; shortcuts: unknown }[]>`
6
+ export const createRailPreferencesService = (injectedDb?: SQL) => {
7
+ const db = () => injectedDb ?? sql;
8
+ return {
9
+ async get(userId: string): Promise<RailPreferences> {
10
+ const [row] = await db()<{ revision: number; visibility: unknown; shortcuts: unknown }[]>`
8
11
  SELECT revision, visibility, shortcuts FROM auth.rail_preferences WHERE user_id = ${userId}::uuid
9
12
  `;
10
- return row ? RailPreferencesSchema.parse(row) : defaultRailPreferences();
11
- },
12
- async save(userId: string, input: RailPreferences): Promise<RailPreferences | null> {
13
- const value = RailPreferencesSchema.parse(input);
14
- const [row] = await db<{ revision: number; visibility: unknown; shortcuts: unknown }[]>`
13
+ return row ? RailPreferencesSchema.parse(row) : defaultRailPreferences();
14
+ },
15
+ async save(userId: string, input: RailPreferences): Promise<RailPreferences | null> {
16
+ const value = RailPreferencesSchema.parse(input);
17
+ const [row] = await db()<{ revision: number; visibility: unknown; shortcuts: unknown }[]>`
15
18
  INSERT INTO auth.rail_preferences (user_id, revision, visibility, shortcuts)
16
19
  SELECT ${userId}::uuid, 1, (${JSON.stringify(value.visibility)}::text)::jsonb, (${JSON.stringify(value.shortcuts)}::text)::jsonb
17
20
  WHERE ${value.revision} = 0 OR EXISTS (SELECT 1 FROM auth.rail_preferences WHERE user_id = ${userId}::uuid)
@@ -21,7 +24,8 @@ export const createRailPreferencesService = (db: SQL = sql) => ({
21
24
  WHERE auth.rail_preferences.revision = ${value.revision}
22
25
  RETURNING revision, visibility, shortcuts
23
26
  `;
24
- return row ? RailPreferencesSchema.parse(row) : null;
25
- },
26
- });
27
+ return row ? RailPreferencesSchema.parse(row) : null;
28
+ },
29
+ };
30
+ };
27
31
  export const railPreferences = createRailPreferencesService();
@@ -1,7 +1,8 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { type RailAdminInput, RailAdminInputSchema, RailAdminSchema, type RailAdminState } from "../contracts/rail-admin";
3
3
  import { RailShortcutSchema } from "../contracts/rail-preferences";
4
4
  import { hasRole, type User } from "../contracts/shared";
5
+ import { sql } from "../runtime/clients";
5
6
  import { buildAccessPrincipalCondition, createAccess } from "../server/services/access";
6
7
 
7
8
  export class RailAdminError extends Error {
@@ -16,8 +17,9 @@ const requireAdmin = (actor: User | undefined) => {
16
17
  if (!actor || !hasRole(actor, "admin")) throw new RailAdminError(403, "Administrator access required");
17
18
  };
18
19
 
19
- export const createRailShortcutsService = (db: SQL = sql) => {
20
- const list = async (query: SQL = db): Promise<RailAdminState> => {
20
+ export const createRailShortcutsService = (injectedDb?: SQL) => {
21
+ const db = () => injectedDb ?? sql;
22
+ const list = async (query: SQL = db()): Promise<RailAdminState> => {
21
23
  const [state] = await query<{ revision: number }[]>`SELECT revision FROM auth.rail_state WHERE singleton`;
22
24
  const rows = await query<{ shortcut: unknown; access: unknown }[]>`
23
25
  SELECT r.shortcut, COALESCE((
@@ -41,7 +43,7 @@ export const createRailShortcutsService = (db: SQL = sql) => {
41
43
  requireAdmin(actor);
42
44
  // One generation switch invalidates every user, including concurrent old refills.
43
45
  // Keep the configuration revision unchanged so open editors remain valid.
44
- await db`UPDATE auth.rail_state SET cache_version = gen_random_uuid() WHERE singleton`;
46
+ await db()`UPDATE auth.rail_state SET cache_version = gen_random_uuid() WHERE singleton`;
45
47
  },
46
48
  async list(actor: User | undefined) {
47
49
  requireAdmin(actor);
@@ -50,7 +52,7 @@ export const createRailShortcutsService = (db: SQL = sql) => {
50
52
  async save(actor: User | undefined, input: RailAdminInput) {
51
53
  requireAdmin(actor);
52
54
  const value = RailAdminInputSchema.parse(input);
53
- return db.begin(async (tx) => {
55
+ return db().begin(async (tx) => {
54
56
  const [state] = await tx<{ revision: number }[]>`SELECT revision FROM auth.rail_state WHERE singleton FOR UPDATE`;
55
57
  if (state?.revision !== value.revision) throw new RailAdminError(409, "App bar configuration changed. Reload before saving.");
56
58
  await tx`DELETE FROM auth.access WHERE id IN (SELECT access_id FROM auth.rail_shortcut_access)`;
@@ -76,13 +78,13 @@ export const createRailShortcutsService = (db: SQL = sql) => {
76
78
  const match = buildAccessPrincipalCondition({
77
79
  subject: { type: "user", userId },
78
80
  columns: {
79
- userId: db`a.user_id`,
80
- groupId: db`a.group_id`,
81
- serviceAccountId: db`a.service_account_id`,
82
- authenticatedOnly: db`a.authenticated_only`,
81
+ userId: db()`a.user_id`,
82
+ groupId: db()`a.group_id`,
83
+ serviceAccountId: db()`a.service_account_id`,
84
+ authenticatedOnly: db()`a.authenticated_only`,
83
85
  },
84
86
  });
85
- const rows = await db<{ shortcut: unknown }[]>`
87
+ const rows = await db()<{ shortcut: unknown }[]>`
86
88
  SELECT r.shortcut FROM auth.rail_shortcuts r WHERE EXISTS (
87
89
  SELECT 1 FROM auth.rail_shortcut_access ra JOIN auth.access a ON a.id = ra.access_id
88
90
  WHERE ra.shortcut_id = r.id AND a.permission = 'read' AND ${match}
@@ -1,5 +1,5 @@
1
- import { RedisClient } from "bun";
2
- import { env } from "../config/env";
1
+ import type { RedisClient } from "bun";
2
+ import { createRedis } from "../runtime/clients";
3
3
 
4
4
  // Cache commands must not queue across a disconnect: delayed fills/invalidations
5
5
  // are not useful, and reads must be able to fall back to Postgres. Keep this
@@ -10,7 +10,7 @@ let reconnecting: Promise<void> | undefined;
10
10
 
11
11
  export const requestCacheRedis = async (): Promise<RedisClient> => {
12
12
  if (!client) {
13
- client = new RedisClient(env.REDIS_URL, { enableOfflineQueue: false });
13
+ client = createRedis({ enableOfflineQueue: false });
14
14
  // The first operation waits for normal connection establishment (Bun's
15
15
  // connection timeout applies). Later disconnected operations fail promptly.
16
16
  initialConnection = client.connect().catch(() => {});
@@ -1,7 +1,8 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import { crypto, err, fail, ok, type PageParams, type Paginated, type Result } from "@k2b/stdlib";
3
- import { type SQL, sql } from "bun";
3
+ import type { SQL } from "bun";
4
4
  import { AI_INFERENCE_SCOPE, type User } from "../contracts/shared";
5
+ import { sql } from "../runtime/clients";
5
6
  import { accounts } from "./accounts";
6
7
  import { audit } from "./audit";
7
8
  import { isUniqueViolation, toPgTextArray } from "./postgres";
@@ -1,5 +1,6 @@
1
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
2
- import { sql } from "bun";
2
+ import { sql } from "../runtime/clients";
3
+
3
4
  import { isUniqueViolation } from "./postgres";
4
5
 
5
6
  export type ServiceAccountKind = "user_delegated" | "resource_bound" | "standalone" | "agent";
@@ -1,10 +1,11 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { Context } from "hono";
3
3
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
4
4
  import { HTTPException } from "hono/http-exception";
5
5
  import { env } from "../../config/env";
6
6
  import { PWA_AUTH_PATH, PWA_COOKIES } from "../../contracts/pwa";
7
7
  import type { User } from "../../contracts/shared";
8
+ import { sql } from "../../runtime/clients";
8
9
  import { getLocale } from "../../server/locale";
9
10
  import { describeRequest } from "../../server/request-context";
10
11
  import { isAccountCategoryAllowed } from "../account-category-policy";
@@ -1,5 +1,6 @@
1
- import { type SQLQuery, sql } from "bun";
1
+ import type { SQLQuery } from "bun";
2
2
  import type { User } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
3
4
  import { decodeAccountCategoryEnabled } from "../account-category-policy";
4
5
  import { buildRoles } from "../accounts/authz";
5
6
  import { resolveProviderProfile } from "../accounts/base-user";
@@ -49,6 +49,26 @@ export const CORE_SETTINGS = {
49
49
  },
50
50
  },
51
51
  },
52
+ "gateway.ingress_cert_resolver": {
53
+ kind: "string",
54
+ default: "le",
55
+ label: "App domain certificate resolver",
56
+ description:
57
+ "Traefik certificate resolver used by app domain routers. Must name a configured ACME resolver; changes apply within five seconds.",
58
+ validate: (value: unknown) =>
59
+ typeof value === "string" && /^[a-zA-Z0-9_-]+$/.test(value)
60
+ ? undefined
61
+ : "Use a Traefik certificate resolver name (letters, digits, underscores or hyphens).",
62
+ presentation: {
63
+ translations: {
64
+ de: {
65
+ label: "Zertifikatsresolver für App-Domains",
66
+ description:
67
+ "Traefik-Zertifikatsresolver der App-Domains. Muss einen konfigurierten ACME-Resolver benennen; Änderungen wirken innerhalb von fünf Sekunden.",
68
+ },
69
+ },
70
+ },
71
+ },
52
72
  "gateway.trusted_proxies": {
53
73
  kind: "string_list",
54
74
  default: DEFAULT_TRUSTED_PROXIES,
@@ -1,3 +1,4 @@
1
+ import { connections } from "../../runtime/connections";
1
2
  /**
2
3
  * Encryption helpers for settings at-rest.
3
4
  *
@@ -7,7 +8,6 @@
7
8
  */
8
9
 
9
10
  import { createHash } from "node:crypto";
10
- import { env } from "../../config/env";
11
11
  import { crypto } from "../../server/services";
12
12
 
13
13
  /**
@@ -34,7 +34,7 @@ import { crypto } from "../../server/services";
34
34
  * with deployments that have always used hex-format APP_SECRET.
35
35
  */
36
36
  export const getAppSecret = (): string => {
37
- const raw = env.APP_SECRET.trim();
37
+ const raw = connections().appSecret.current.trim();
38
38
  if (!raw) {
39
39
  throw new Error("APP_SECRET is required to read or write encrypted settings");
40
40
  }